跳转至

算法与数据结构面试题 - 50道高频LeetCode精选

算法与数据结构面试题图

本文精选50道高频LeetCode面试题,按类型分类整理,每题包含思路分析、Python实现、复杂度分析和面试追问。


一、数组与字符串(15题)


1. 两数之和(Two Sum)

题目描述: 给定一个整数数组 nums 和一个整数目标值 target,请在数组中找出和为目标值的两个整数,返回它们的数组下标。假设每种输入只有一个答案,且同一个元素不能重复使用。

思路分析:

  • 暴力法: 双层循环遍历所有组合,时间复杂度 O(n²)
  • 优化 - 哈希表法: 遍历数组时,用哈希表记录已遍历的值和下标。对于当前元素 num,检查 target - num 是否已在哈希表中。一次遍历即可完成。
Python
def twoSum(nums: list[int], target: int) -> list[int]:
    # 哈希表:值 -> 下标
    seen = {}
    for i, num in enumerate(nums):
        complement = target - num
        if complement in seen:
            return [seen[complement], i]
        seen[num] = i
    return []

复杂度分析: - 时间复杂度:O(n),一次遍历 - 空间复杂度:O(n),哈希表存储

面试追问: - 如果数组已排序?→ 双指针法,O(1) 空间 - 如果要返回所有满足条件的组合?→ 需要去重处理 - 如果有多个答案怎么办?→ 收集所有结果对


2. 三数之和(3Sum)

题目描述: 给定整数数组 nums,找出所有和为 0 的三元组 [nums[i], nums[j], nums[k]],使得 i ≠ j ≠ k,且答案中不包含重复三元组。

思路分析:

  • 暴力法: 三层循环 O(n³),再去重
  • 优化 - 排序 + 双指针: 先排序,固定一个数,用双指针在剩余区间查找。排序后方便跳过重复元素。
Python
def threeSum(nums: list[int]) -> list[list[int]]:
    nums.sort()
    result = []
    n = len(nums)

    for i in range(n - 2):
        # 跳过重复的第一个数
        if i > 0 and nums[i] == nums[i - 1]:
            continue
        # 剪枝:最小值已大于0,后面不可能有解
        if nums[i] > 0:
            break

        left, right = i + 1, n - 1
        while left < right:
            total = nums[i] + nums[left] + nums[right]
            if total == 0:
                result.append([nums[i], nums[left], nums[right]])
                # 跳过重复值
                while left < right and nums[left] == nums[left + 1]:
                    left += 1
                while left < right and nums[right] == nums[right - 1]:
                    right -= 1
                left += 1
                right -= 1
            elif total < 0:
                left += 1
            else:
                right -= 1

    return result

复杂度分析: - 时间复杂度:O(n²),排序 O(n log n) + 双指针 O(n²) - 空间复杂度:O(1),不计输出空间

面试追问: - 四数之和怎么做?→ 外层再加一层循环,或递归降维 - 最接近的三数之和?→ 维护一个最小差值 - 如果允许重复元素出现在不同位置?→ 不去重即可


3. 最长无重复子串(Longest Substring Without Repeating Characters)

题目描述: 给定一个字符串 s,找出其中不含重复字符的最长子串的长度。

思路分析:

  • 暴力法: 枚举所有子串,检查是否无重复 O(n³)
  • 优化 - 滑动窗口: 维护一个窗口 [left, right],用集合/哈希表记录窗口内字符。遇到重复字符时,移动左边界直到窗口内无重复。
Python
def lengthOfLongestSubstring(s: str) -> int:
    # 字符 -> 最近出现的下标
    char_index = {}
    max_len = 0
    left = 0

    for right, char in enumerate(s):
        # 如果字符已在窗口内,移动左边界
        if char in char_index and char_index[char] >= left:
            left = char_index[char] + 1
        char_index[char] = right
        max_len = max(max_len, right - left + 1)

    return max_len

复杂度分析: - 时间复杂度:O(n),每个字符最多被访问两次 - 空间复杂度:O(min(m, n)),m 为字符集大小

面试追问: - 最长无重复子串本身是什么(而非长度)?→ 记录起始位置 - 如果要求至少包含 k 个不同字符?→ 变形滑动窗口 - 如果字符串包含 Unicode?→ 哈希表天然支持


4. 滑动窗口最大值(Sliding Window Maximum)

题目描述: 给定整数数组 nums 和窗口大小 k,窗口从最左端滑到最右端,返回每个窗口位置的最大值。

思路分析:

  • 暴力法: 每次取窗口内最大值 O(nk)
  • 优化 - 单调队列(双端队列): 维护一个单调递减的双端队列,队首始终是当前窗口最大值的下标。新元素入队时,从队尾移除所有比它小的元素。
Python
from collections import deque

def maxSlidingWindow(nums: list[int], k: int) -> list[int]:
    dq = deque()  # 存储下标,对应值单调递减
    result = []

    for i, num in enumerate(nums):
        # 移除超出窗口范围的队首元素
        while dq and dq[0] < i - k + 1:
            dq.popleft()
        # 维护单调递减:移除队尾所有比当前值小的元素
        while dq and nums[dq[-1]] < num:
            dq.pop()
        dq.append(i)
        # 窗口形成后,记录最大值
        if i >= k - 1:
            result.append(nums[dq[0]])

    return result

复杂度分析: - 时间复杂度:O(n),每个元素最多入队出队各一次 - 空间复杂度:O(k),双端队列最多存 k 个元素

面试追问: - 如果需要滑动窗口最小值?→ 改为单调递增队列 - 用堆如何实现?→ 最大堆 + 懒删除,O(n log n) - 如何处理窗口大小动态变化的情况?


5. 合并区间(Merge Intervals)

题目描述: 给定若干区间 intervals,合并所有重叠的区间。

思路分析:

  • 先按区间起始位置排序,然后依次比较当前区间与结果中最后一个区间是否重叠。
Python
def merge(intervals: list[list[int]]) -> list[list[int]]:
    # 按起始位置排序
    intervals.sort(key=lambda x: x[0])  # lambda匿名函数:简洁的单行函数
    merged = []

    for interval in intervals:
        # 如果结果为空,或当前区间与最后一个不重叠
        if not merged or merged[-1][1] < interval[0]:  # 负索引:从末尾倒数访问元素
            merged.append(interval)
        else:
            # 合并:更新右端点
            merged[-1][1] = max(merged[-1][1], interval[1])

    return merged

复杂度分析: - 时间复杂度:O(n log n),排序主导 - 空间复杂度:O(n),存储结果

面试追问: - 插入一个新区间到已排序的区间列表?→ 二分查找 + 合并 - 区间交集如何计算?→ 双指针遍历两个已排序列表 - 如果区间是以流的方式到来?→ 在线算法 / 区间树


6. 接雨水(Trapping Rain Water)

题目描述: 给定 n 个非负整数表示柱子高度(宽度为1),计算下雨后能接多少雨水。

思路分析:

  • 暴力法: 对每个位置,找左右最高柱子,取较小值减去当前高度 O(n²)
  • 优化1 - 预处理数组: 预计算每个位置左边最大值和右边最大值 O(n)
  • 优化2 - 双指针: 左右指针向中间移动,维护左右最大高度
Python
def trap(height: list[int]) -> int:
    if not height:
        return 0

    left, right = 0, len(height) - 1
    left_max, right_max = 0, 0
    water = 0

    while left < right:
        if height[left] < height[right]:
            if height[left] >= left_max:
                left_max = height[left]
            else:
                water += left_max - height[left]
            left += 1
        else:
            if height[right] >= right_max:
                right_max = height[right]
            else:
                water += right_max - height[right]
            right -= 1

    return water

复杂度分析: - 时间复杂度:O(n) - 空间复杂度:O(1),双指针法

面试追问: - 用栈如何实现?→ 单调递减栈,横向计算 - 二维接雨水(3D版本)?→ BFS + 最小堆 - 柱子宽度不为1时如何处理?


7. 最长回文子串(Longest Palindromic Substring)

题目描述: 给定字符串 s,找到最长的回文子串。

思路分析:

  • 暴力法: 枚举所有子串检查回文 O(n³)
  • 优化 - 中心扩展法: 以每个字符(或两个字符间隙)为中心向两边扩展。回文中心有 2n-1 个。
  • 最优 - Manacher算法: O(n) 但面试中中心扩展法足够。
Python
def longestPalindrome(s: str) -> str:
    if len(s) < 2:
        return s

    start, max_len = 0, 1

    def expand_around_center(left: int, right: int):
        """从中心向两边扩展,返回回文长度"""
        while left >= 0 and right < len(s) and s[left] == s[right]:
            left -= 1
            right += 1
        return right - left - 1

    for i in range(len(s)):
        # 奇数长度回文
        len1 = expand_around_center(i, i)
        # 偶数长度回文
        len2 = expand_around_center(i, i + 1)
        cur_len = max(len1, len2)
        if cur_len > max_len:
            max_len = cur_len
            start = i - (cur_len - 1) // 2

    return s[start:start + max_len]

复杂度分析: - 时间复杂度:O(n²),中心扩展 - 空间复杂度:O(1)

面试追问: - Manacher 算法原理?→ 利用已知回文的对称性 - 最长回文子序列?→ DP,dp[i][j] 表示 s[i..j] 的最长回文子序列长度 - 回文划分最少次数?→ DP


8. 螺旋矩阵(Spiral Matrix)

题目描述: 给定一个 m × n 矩阵,按螺旋顺序返回所有元素。

思路分析:

按层模拟,定义上下左右四个边界,按"右→下→左→上"的顺序遍历,每遍历完一条边就收缩对应边界。

Python
def spiralOrder(matrix: list[list[int]]) -> list[int]:
    if not matrix:
        return []

    result = []
    top, bottom = 0, len(matrix) - 1
    left, right = 0, len(matrix[0]) - 1

    while top <= bottom and left <= right:
        # 向右遍历
        for col in range(left, right + 1):
            result.append(matrix[top][col])
        top += 1

        # 向下遍历
        for row in range(top, bottom + 1):
            result.append(matrix[row][right])
        right -= 1

        # 向左遍历(需检查边界)
        if top <= bottom:
            for col in range(right, left - 1, -1):
                result.append(matrix[bottom][col])
            bottom -= 1

        # 向上遍历(需检查边界)
        if left <= right:
            for row in range(bottom, top - 1, -1):
                result.append(matrix[row][left])
            left += 1

    return result

复杂度分析: - 时间复杂度:O(m × n) - 空间复杂度:O(1),不计输出

面试追问: - 螺旋矩阵II(生成螺旋矩阵)?→ 反向填充 - 逆时针螺旋?→ 调整遍历方向 - 对角线遍历?→ 按对角线分组


9. 旋转图像(Rotate Image)

题目描述: 给定 n × n 矩阵,将其顺时针旋转 90 度,要求原地旋转。

思路分析:

  • 方法1: 先转置(行变列),再左右翻转每行
  • 方法2: 四点轮换,每次处理一圈
Python
def rotate(matrix: list[list[int]]) -> None:
    n = len(matrix)

    # 第一步:转置矩阵(沿主对角线翻转)
    for i in range(n):
        for j in range(i + 1, n):
            matrix[i][j], matrix[j][i] = matrix[j][i], matrix[i][j]

    # 第二步:水平翻转每一行
    for i in range(n):
        matrix[i].reverse()

复杂度分析: - 时间复杂度:O(n²) - 空间复杂度:O(1),原地旋转

面试追问: - 逆时针旋转90度?→ 先转置,再上下翻转 - 旋转180度?→ 先上下翻转,再左右翻转 - 如果不是方阵?→ 无法原地,需要新矩阵


10. 缺失的第一个正数(First Missing Positive)

题目描述: 给定未排序整数数组 nums,找出其中没有出现的最小正整数。要求时间 O(n),空间 O(1)。

思路分析:

  • 原地哈希: 利用数组自身作为哈希表。将每个值 v(1 ≤ v ≤ n)放到下标 v-1 的位置。遍历后第一个 nums[i] ≠ i+1 的位置就是答案。
Python
def firstMissingPositive(nums: list[int]) -> int:
    n = len(nums)

    # 将每个数放到正确的位置:nums[i] = i + 1
    for i in range(n):
        while 1 <= nums[i] <= n and nums[nums[i] - 1] != nums[i]:
            # 交换 nums[i] 到正确位置
            correct_idx = nums[i] - 1
            nums[i], nums[correct_idx] = nums[correct_idx], nums[i]

    # 找第一个不在正确位置的数
    for i in range(n):
        if nums[i] != i + 1:
            return i + 1

    return n + 1

复杂度分析: - 时间复杂度:O(n),每个元素最多被交换一次 - 空间复杂度:O(1)

面试追问: - 为什么 while 循环不会超过 O(n)?→ 每次交换至少让一个元素归位 - 如果不允许修改原数组?→ 需要 O(n) 空间 - 缺失的前 k 个正数?→ 类似思路扩展


11. 最小覆盖子串(Minimum Window Substring)

题目描述: 给定字符串 st,在 s 中找出包含 t 所有字符的最小子串。

思路分析:

滑动窗口 + 计数器。右指针扩展窗口,当窗口满足条件时,左指针收缩寻找更小的窗口。

Python
from collections import Counter

def minWindow(s: str, t: str) -> str:
    if not s or not t:
        return ""

    need = Counter(t)        # 需要的字符计数
    missing = len(t)         # 还缺少的字符数
    left = 0
    start, min_len = 0, float('inf')

    for right, char in enumerate(s):
        # 扩展窗口
        if need[char] > 0:
            missing -= 1
        need[char] -= 1

        # 收缩窗口
        while missing == 0:
            window_len = right - left + 1
            if window_len < min_len:
                min_len = window_len
                start = left

            need[s[left]] += 1
            if need[s[left]] > 0:
                missing += 1
            left += 1

    return s[start:start + min_len] if min_len != float('inf') else ""

复杂度分析: - 时间复杂度:O(|s| + |t|) - 空间复杂度:O(|字符集|)

面试追问: - 如果要求子串中字符顺序与 t 一致?→ 子序列匹配问题 - 最小窗口包含 t 的所有字符至少 k 次?→ 修改 need 计数 - 有多个最小窗口怎么返回?→ 收集所有长度等于 min_len 的窗口


12. 字符串转整数(atoi)

题目描述: 实现 myAtoi(string s) 函数,将字符串转换为 32 位有符号整数。

思路分析:

模拟题,注意边界条件:前导空格、正负号、非数字字符、溢出处理。

Python
def myAtoi(s: str) -> int:
    INT_MAX, INT_MIN = 2**31 - 1, -(2**31)

    s = s.lstrip()  # 去前导空格
    if not s:
        return 0

    sign = 1
    idx = 0

    # 处理正负号
    if s[0] in ('+', '-'):
        sign = -1 if s[0] == '-' else 1
        idx = 1

    result = 0
    while idx < len(s) and s[idx].isdigit():
        digit = int(s[idx])
        # 溢出检查
        if result > (INT_MAX - digit) // 10:
            return INT_MAX if sign == 1 else INT_MIN
        result = result * 10 + digit
        idx += 1

    return sign * result

复杂度分析: - 时间复杂度:O(n) - 空间复杂度:O(1)

面试追问: - 用有限状态自动机(DFA)如何实现?→ 定义状态转移表 - 如何支持十六进制转换?→ 识别 "0x" 前缀 - 浮点数转换如何处理?→ 增加小数点和指数处理


13. 最长公共前缀(Longest Common Prefix)

题目描述: 编写函数来查找字符串数组中的最长公共前缀。

思路分析:

  • 纵向扫描: 逐个字符比较所有字符串的同一位置
  • 分治法: 递归地找左半和右半的公共前缀,再合并
Python
def longestCommonPrefix(strs: list[str]) -> str:
    if not strs:
        return ""

    # 纵向扫描
    for i in range(len(strs[0])):
        char = strs[0][i]
        for s in strs[1:]:
            if i >= len(s) or s[i] != char:
                return strs[0][:i]

    return strs[0]

复杂度分析: - 时间复杂度:O(S),S 为所有字符串字符总数 - 空间复杂度:O(1)

面试追问: - 如果字符串非常多,用 Trie 如何优化?→ 沿唯一子节点路径找 - 二分查找法?→ 二分前缀长度,验证是否所有字符串都有该前缀 - 最长公共后缀?→ 反转所有字符串后求最长公共前缀


14. 移动零(Move Zeroes)

题目描述: 给定数组 nums,将所有 0 移到末尾,同时保持非零元素的相对顺序,要求原地操作。

思路分析:

双指针:slow 指向下一个非零元素应放的位置,fast 遍历数组。

Python
def moveZeroes(nums: list[int]) -> None:
    slow = 0
    for fast in range(len(nums)):
        if nums[fast] != 0:
            nums[slow], nums[fast] = nums[fast], nums[slow]
            slow += 1

复杂度分析: - 时间复杂度:O(n) - 空间复杂度:O(1)

面试追问: - 最少交换次数?→ 统计非零元素前面的零的个数 - 移动特定值到末尾?→ 修改判断条件 - 移除所有零(不保留)?→ 只写不交换


15. 下一个排列(Next Permutation)

题目描述: 实现获取下一个排列的函数,将数字重新排列成字典序中下一个更大的排列。如果不存在,则排列为最小的排列(升序)。

思路分析:

  1. 从右往左找到第一个 nums[i] < nums[i+1] 的位置 i
  2. 从右往左找到第一个 nums[j] > nums[i] 的位置 j
  3. 交换 nums[i]nums[j]
  4. 翻转 i+1 之后的部分
Python
def nextPermutation(nums: list[int]) -> None:
    n = len(nums)

    # 第一步:从右往左找第一个递减位置
    i = n - 2
    while i >= 0 and nums[i] >= nums[i + 1]:
        i -= 1

    if i >= 0:
        # 第二步:从右往左找第一个大于 nums[i] 的数
        j = n - 1
        while nums[j] <= nums[i]:
            j -= 1
        # 第三步:交换
        nums[i], nums[j] = nums[j], nums[i]

    # 第四步:翻转 i+1 之后的部分
    left, right = i + 1, n - 1
    while left < right:
        nums[left], nums[right] = nums[right], nums[left]
        left += 1
        right -= 1

复杂度分析: - 时间复杂度:O(n) - 空间复杂度:O(1)

面试追问: - 上一个排列?→ 相反操作:找第一个递增位置 - 第 k 个排列?→ 数学方法,逐位确定 - 排列总数?→ n! / (重复元素阶乘)


二、链表(8题)


16. 反转链表(Reverse Linked List)

题目描述: 给定单链表的头节点 head,反转链表并返回反转后的链表头。

思路分析:

  • 迭代法: 用三个指针 prevcurrnext 逐个翻转
  • 递归法: 递归到链表末尾,回溯时翻转指向
Python
class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next

def reverseList(head: ListNode) -> ListNode:
    # 迭代法
    prev, curr = None, head
    while curr:
        next_node = curr.next   # 暂存下一个节点
        curr.next = prev        # 翻转指向
        prev = curr             # prev 前进
        curr = next_node        # curr 前进
    return prev

def reverseListRecursive(head: ListNode) -> ListNode:
    # 递归法
    if not head or not head.next:
        return head
    new_head = reverseListRecursive(head.next)
    head.next.next = head    # 后继节点指回自己
    head.next = None         # 断开原来的指向
    return new_head

复杂度分析: - 时间复杂度:O(n) - 空间复杂度:迭代 O(1),递归 O(n)

面试追问: - 反转链表的第 m 到第 n 个节点?→ 定位后局部反转 - 每 k 个一组反转?→ 分组处理 + 连接 - 判断反转后是否与原链表相同(回文链表)?→ 反转后半部分比较


17. 合并K个排序链表(Merge k Sorted Lists)

题目描述: 给定 k 个升序排列的链表,合并为一个升序链表。

思路分析:

  • 暴力法: 收集所有值排序后建链表 O(N log N)
  • 优化1 - 最小堆: 将每个链表头节点入堆,每次弹出最小节点,将其后继入堆
  • 优化2 - 分治合并: 两两合并,递归或迭代
Python
import heapq

def mergeKLists(lists: list[ListNode]) -> ListNode:
    # 最小堆方法
    dummy = ListNode(0)
    curr = dummy
    heap = []

    # 初始化:每个链表的头节点入堆
    for i, node in enumerate(lists):
        if node:
            heapq.heappush(heap, (node.val, i, node))

    while heap:
        val, idx, node = heapq.heappop(heap)
        curr.next = node
        curr = curr.next
        if node.next:
            heapq.heappush(heap, (node.next.val, idx, node.next))

    return dummy.next

复杂度分析: - 时间复杂度:O(N log k),N 为总节点数,k 为链表数 - 空间复杂度:O(k),堆大小

面试追问: - 分治法的时间复杂度推导?→ T(k) = 2T(k/2) + O(N) - 如果链表不是排序的?→ 先各自排序或全收集排序 - 外部排序的联系?→ 多路归并正是核心思想


18. 环形链表 II(Linked List Cycle II)

题目描述: 给定链表头节点 head,如果链表中有环,返回环的入口节点;否则返回 null

思路分析:

Floyd 判圈算法: 1. 快慢指针相遇,确认有环 2. 将一个指针移回头部,两个指针同速前进,再次相遇处就是入口

数学推导: 设头到入口距离为 a,入口到相遇点为 b,环长为 c。快指针走了 a+b+nc,慢指针走了 a+b。因为快指针速度是慢指针2倍:2(a+b) = a+b+nc,所以 a = nc - b = (n-1)c + (c-b)

Python
def detectCycle(head: ListNode) -> ListNode:
    slow = fast = head

    # 第一步:快慢指针判断是否有环
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
        if slow == fast:
            # 第二步:找入口
            slow = head
            while slow != fast:
                slow = slow.next
                fast = fast.next
            return slow

    return None

复杂度分析: - 时间复杂度:O(n) - 空间复杂度:O(1)

面试追问: - 如何求环的长度?→ 相遇后一个指针继续走,计数直到再次相遇 - 如何判断两个链表是否相交(都可能有环)?→ 分情况讨论 - 为什么快指针速度必须是2倍?→ 保证在环内必然相遇


19. LRU缓存(LRU Cache)

题目描述: 设计并实现一个 LRU(最近最少使用)缓存机制。支持 get(key)put(key, value) 操作,均要求 O(1) 时间复杂度。

思路分析:

哈希表 + 双向链表: 哈希表提供 O(1) 查找,双向链表维护访问顺序。最近使用的放头部,淘汰时从尾部移除。

Python
class DLinkedNode:
    def __init__(self, key=0, value=0):
        self.key = key
        self.value = value
        self.prev = None
        self.next = None

class LRUCache:
    def __init__(self, capacity: int):
        self.cache = {}           # key -> DLinkedNode
        self.capacity = capacity
        self.size = 0
        # 伪头部和伪尾部
        self.head = DLinkedNode()
        self.tail = DLinkedNode()
        self.head.next = self.tail
        self.tail.prev = self.head

    def get(self, key: int) -> int:
        if key not in self.cache:
            return -1
        node = self.cache[key]
        self._move_to_head(node)  # 标记为最近使用
        return node.value

    def put(self, key: int, value: int) -> None:
        if key in self.cache:
            node = self.cache[key]
            node.value = value
            self._move_to_head(node)
        else:
            node = DLinkedNode(key, value)
            self.cache[key] = node
            self._add_to_head(node)
            self.size += 1
            if self.size > self.capacity:
                # 移除最久未使用的节点
                removed = self._remove_tail()
                del self.cache[removed.key]
                self.size -= 1

    def _add_to_head(self, node):
        node.prev = self.head
        node.next = self.head.next
        self.head.next.prev = node
        self.head.next = node

    def _remove_node(self, node):
        node.prev.next = node.next
        node.next.prev = node.prev

    def _move_to_head(self, node):
        self._remove_node(node)
        self._add_to_head(node)

    def _remove_tail(self):
        node = self.tail.prev
        self._remove_node(node)
        return node

复杂度分析: - 时间复杂度:O(1),get/put 均为常数时间 - 空间复杂度:O(capacity)

面试追问: - LFU 缓存如何实现?→ 多个频率桶 + 双向链表 - 线程安全的 LRU?→ 加锁或使用 ConcurrentHashMap - Python 中 OrderedDict 能否直接实现?→ 可以,move_to_end + popitem


20. 两数相加(Add Two Numbers)

题目描述: 两个非空链表表示两个非负整数(逆序存储),将两数相加返回结果链表。

思路分析:

模拟加法,逐位相加,处理进位。

Python
def addTwoNumbers(l1: ListNode, l2: ListNode) -> ListNode:
    dummy = ListNode(0)
    curr = dummy
    carry = 0

    while l1 or l2 or carry:
        val1 = l1.val if l1 else 0
        val2 = l2.val if l2 else 0
        total = val1 + val2 + carry
        carry = total // 10
        curr.next = ListNode(total % 10)
        curr = curr.next
        l1 = l1.next if l1 else None
        l2 = l2.next if l2 else None

    return dummy.next

复杂度分析: - 时间复杂度:O(max(m, n)) - 空间复杂度:O(max(m, n))

面试追问: - 如果数字是正序存储的?→ 先反转或用栈 - 如何处理非常大的数(超过 long 范围)?→ 链表天然支持大数 - 相减怎么做?→ 需要比较大小 + 借位


21. 删除链表的倒数第 N 个节点(Remove Nth Node From End)

题目描述: 给定链表头节点,删除链表的倒数第 n 个节点,返回链表头。

思路分析:

快慢指针: 快指针先走 n 步,然后快慢同时走,快指针到达末尾时慢指针指向倒数第 n+1 个节点。

Python
def removeNthFromEnd(head: ListNode, n: int) -> ListNode:
    dummy = ListNode(0, head)
    fast = slow = dummy

    # 快指针先走 n+1 步
    for _ in range(n + 1):
        fast = fast.next

    # 同时移动
    while fast:
        fast = fast.next
        slow = slow.next

    # 删除目标节点
    slow.next = slow.next.next
    return dummy.next

复杂度分析: - 时间复杂度:O(L),L 为链表长度 - 空间复杂度:O(1)

面试追问: - 一趟扫描实现?→ 上述方法就是一趟 - 如果 n 大于链表长度?→ 需要先验证 - 删除倒数第 n 个到倒数第 m 个?→ 定位两个位置


22. 链表排序(Sort List)

题目描述: 给定链表头节点,将其按升序排序。要求 O(n log n) 时间和 O(1) 空间。

思路分析:

归并排序(自底向上): 避免递归带来的 O(log n) 栈空间。

Python
def sortList(head: ListNode) -> ListNode:
    # 递归归并排序(简洁版,空间 O(log n))
    if not head or not head.next:
        return head

    # 快慢指针找中点
    slow, fast = head, head.next
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next

    mid = slow.next
    slow.next = None  # 断开

    # 递归排序两半
    left = sortList(head)
    right = sortList(mid)

    # 合并
    dummy = ListNode(0)
    curr = dummy
    while left and right:
        if left.val <= right.val:
            curr.next = left
            left = left.next
        else:
            curr.next = right
            right = right.next
        curr = curr.next
    curr.next = left or right

    return dummy.next

复杂度分析: - 时间复杂度:O(n log n) - 空间复杂度:O(log n),递归栈;自底向上可做到 O(1)

面试追问: - 自底向上归并排序的实现?→ 循环 subLength = 1, 2, 4, ... - 为什么不用快排?→ 链表随机访问慢,归并更适合 - 稳定排序?→ 归并排序是稳定的


23. 相交链表(Intersection of Two Linked Lists)

题目描述: 找到两个单链表相交的起始节点。

思路分析:

双指针法: 指针 A 走完链表 A 后走链表 B,指针 B 走完链表 B 后走链表 A。如果相交,两指针会在交点相遇;如果不相交,同时到达 null。

Python
def getIntersectionNode(headA: ListNode, headB: ListNode) -> ListNode:
    pA, pB = headA, headB
    while pA != pB:
        pA = pA.next if pA else headB
        pB = pB.next if pB else headA
    return pA

复杂度分析: - 时间复杂度:O(m + n) - 空间复杂度:O(1)

面试追问: - 为什么一定会在交点相遇?→ 两指针都走了 m + n 的距离 - 如果要找所有交叉点?→ 链表只能有一个交叉起点 - 如果链表中有环怎么办?→ 先判环再处理


三、树(10题)


24. 二叉树的层序遍历(Binary Tree Level Order Traversal)

题目描述: 给定二叉树根节点,返回其节点值的层序遍历结果(逐层从左到右)。

思路分析:

BFS,使用队列逐层处理。

Python
from collections import deque

class TreeNode:
    def __init__(self, val=0, left=None, right=None):
        self.val = val
        self.left = left
        self.right = right

def levelOrder(root: TreeNode) -> list[list[int]]:
    if not root:
        return []

    result = []
    queue = deque([root])

    while queue:
        level_size = len(queue)
        level = []
        for _ in range(level_size):
            node = queue.popleft()
            level.append(node.val)
            if node.left:
                queue.append(node.left)
            if node.right:
                queue.append(node.right)
        result.append(level)

    return result

复杂度分析: - 时间复杂度:O(n) - 空间复杂度:O(n),队列最大宽度

面试追问: - 锯齿形层序遍历?→ 奇数层反转 - 层序遍历的倒序?→ 结果列表 reverse - N 叉树层序遍历?→ 遍历所有子节点


25. 验证二叉搜索树(Validate BST)

题目描述: 判断给定二叉树是否是有效的二叉搜索树。

思路分析:

  • 递归 + 范围约束: 每个节点的值必须在 (min_val, max_val) 范围内
  • 中序遍历: BST 的中序遍历结果是严格递增的
Python
def isValidBST(root: TreeNode) -> bool:
    def validate(node, min_val, max_val):
        if not node:
            return True
        if node.val <= min_val or node.val >= max_val:
            return False
        return (validate(node.left, min_val, node.val) and
                validate(node.right, node.val, max_val))

    return validate(root, float('-inf'), float('inf'))

复杂度分析: - 时间复杂度:O(n) - 空间复杂度:O(h),h 为树高

面试追问: - 如果允许重复值怎么办?→ 明确重复值放左还是右 - 中序遍历法的优势?→ 可以用 Morris 遍历实现 O(1) 空间 - 恢复一棵被交换了两个节点的 BST?→ 中序遍历找两个逆序对


26. 二叉树的最近公共祖先(Lowest Common Ancestor)

题目描述: 给定二叉树和两个节点 p、q,找到它们的最近公共祖先。

思路分析:

递归:如果当前节点是 p 或 q,返回当前节点。递归左右子树,如果 p 和 q 分别在左右子树中,当前节点就是 LCA。

Python
def lowestCommonAncestor(root: TreeNode, p: TreeNode, q: TreeNode) -> TreeNode:
    if not root or root == p or root == q:
        return root

    left = lowestCommonAncestor(root.left, p, q)
    right = lowestCommonAncestor(root.right, p, q)

    if left and right:
        return root      # p 和 q 分别在左右子树
    return left or right  # 都在同一侧

复杂度分析: - 时间复杂度:O(n) - 空间复杂度:O(h)

面试追问: - 如果是 BST 呢?→ 利用 BST 性质,O(h) 时间 - 如果节点有父指针?→ 转化为链表相交问题 - 多个节点的最近公共祖先?→ 两两求 LCA


27. 二叉树的序列化与反序列化(Serialize and Deserialize Binary Tree)

题目描述: 设计一个算法来序列化和反序列化二叉树。

思路分析:

使用前序遍历,用特殊标记(如 "#")表示 null 节点。

Python
class Codec:
    def serialize(self, root: TreeNode) -> str:
        """前序遍历序列化"""
        result = []

        def dfs(node):
            if not node:
                result.append("#")
                return
            result.append(str(node.val))
            dfs(node.left)
            dfs(node.right)

        dfs(root)
        return ",".join(result)

    def deserialize(self, data: str) -> TreeNode:
        """反序列化"""
        values = iter(data.split(","))

        def dfs():
            val = next(values)
            if val == "#":
                return None
            node = TreeNode(int(val))
            node.left = dfs()
            node.right = dfs()
            return node

        return dfs()

复杂度分析: - 时间复杂度:O(n) - 空间复杂度:O(n)

面试追问: - 层序遍历序列化?→ BFS + null 填充 - 如何压缩序列化结果?→ 二进制编码 / 位运算 - N 叉树如何序列化?→ 记录子节点数量


28. 二叉树中的最大路径和(Binary Tree Maximum Path Sum)

题目描述: 给定二叉树,找出任意节点到任意节点的路径的最大路径和。路径不一定经过根节点。

思路分析:

后序遍历,对于每个节点计算经过它的最大路径和。递归返回的是单侧最大贡献值(不能拐弯)。

Python
def maxPathSum(root: TreeNode) -> int:
    max_sum = float('-inf')

    def max_gain(node):
        nonlocal max_sum
        if not node:
            return 0

        # 左右子树的最大贡献值(负数贡献不要)
        left_gain = max(max_gain(node.left), 0)
        right_gain = max(max_gain(node.right), 0)

        # 经过当前节点的最大路径和
        price_new_path = node.val + left_gain + right_gain
        max_sum = max(max_sum, price_new_path)

        # 返回单侧最大贡献值
        return node.val + max(left_gain, right_gain)

    max_gain(root)
    return max_sum

复杂度分析: - 时间复杂度:O(n) - 空间复杂度:O(h)

面试追问: - 如何输出最大路径本身?→ 记录路径节点 - 路径必须从叶到叶?→ 修改 gain 不能取 0 - 路径至少包含 k 个节点?→ 增加约束条件


29. 从前序与中序遍历构造二叉树

题目描述: 根据前序遍历和中序遍历的结果,构造二叉树。

思路分析:

前序的第一个元素是根。在中序中找到根的位置,左边是左子树,右边是右子树。递归构建。

Python
def buildTree(preorder: list[int], inorder: list[int]) -> TreeNode:
    # 中序值 -> 下标映射,加速查找
    inorder_map = {val: idx for idx, val in enumerate(inorder)}  # enumerate同时获取索引和值
    pre_idx = 0

    def helper(in_left, in_right):
        nonlocal pre_idx
        if in_left > in_right:
            return None

        root_val = preorder[pre_idx]
        root = TreeNode(root_val)
        pre_idx += 1

        # 在中序中找到根的位置
        in_root = inorder_map[root_val]

        # 先构建左子树,再构建右子树(与前序遍历顺序一致)
        root.left = helper(in_left, in_root - 1)
        root.right = helper(in_root + 1, in_right)

        return root

    return helper(0, len(inorder) - 1)

复杂度分析: - 时间复杂度:O(n) - 空间复杂度:O(n),哈希表

面试追问: - 从中序和后序构建?→ 后序最后一个是根,从右往左处理 - 如果有重复值?→ 无法唯一确定树结构 - 从前序和后序能否确定唯一二叉树?→ 不能,除非是满二叉树


30. 二叉树的直径(Diameter of Binary Tree)

题目描述: 给定二叉树,计算它的直径(任意两节点之间最长路径的长度)。

Python
def diameterOfBinaryTree(root: TreeNode) -> int:
    diameter = 0

    def depth(node):
        nonlocal diameter
        if not node:
            return 0
        left = depth(node.left)
        right = depth(node.right)
        diameter = max(diameter, left + right)
        return 1 + max(left, right)

    depth(root)
    return diameter

复杂度分析: - 时间复杂度:O(n) - 空间复杂度:O(h)

面试追问: - 直径不一定经过根节点,如何理解?→ 每个节点都可能是"拐点" - N 叉树的直径?→ 取最大的两个子树深度之和 - 带权边的直径?→ 边权累加


31. 翻转二叉树(Invert Binary Tree)

题目描述: 翻转一棵二叉树(镜像)。

Python
def invertTree(root: TreeNode) -> TreeNode:
    if not root:
        return None
    root.left, root.right = root.right, root.left
    invertTree(root.left)
    invertTree(root.right)
    return root

复杂度分析: - 时间复杂度:O(n) - 空间复杂度:O(h)

面试追问: - 迭代实现?→ BFS 或 DFS + 栈 - 判断两棵树是否互为镜像?→ 对称树判断 - 原地翻转后如何恢复?→ 再翻转一次


32. 二叉树的右视图(Binary Tree Right Side View)

题目描述: 给定二叉树,返回从右侧看到的节点值(每层最后一个节点)。

Python
def rightSideView(root: TreeNode) -> list[int]:
    if not root:
        return []

    result = []
    queue = deque([root])

    while queue:
        level_size = len(queue)
        for i in range(level_size):
            node = queue.popleft()
            if i == level_size - 1:  # 每层最后一个节点
                result.append(node.val)
            if node.left:
                queue.append(node.left)
            if node.right:
                queue.append(node.right)

    return result

复杂度分析: - 时间复杂度:O(n) - 空间复杂度:O(n)

面试追问: - DFS 如何实现?→ 先访问右子树,记录每层第一次访问的节点 - 左视图?→ 取每层第一个节点 - 俯视图/仰视图?→ 列坐标的最上/最下节点


33. 完全二叉树的节点个数(Count Complete Tree Nodes)

题目描述: 给定完全二叉树,求其节点个数。要求优于 O(n)。

思路分析:

利用完全二叉树性质:如果左子树高度等于右子树高度,则左子树是满二叉树。

Python
def countNodes(root: TreeNode) -> int:
    if not root:
        return 0

    left_height = right_height = 0
    left_node, right_node = root, root

    while left_node:
        left_height += 1
        left_node = left_node.left
    while right_node:
        right_height += 1
        right_node = right_node.right

    # 如果高度相同,是满二叉树
    if left_height == right_height:
        return 2**left_height - 1

    return 1 + countNodes(root.left) + countNodes(root.right)

复杂度分析: - 时间复杂度:O(log²n) - 空间复杂度:O(log n)

面试追问: - 二分搜索方法?→ 二分最后一层节点,O(log²n) - 如何判断第 k 个节点是否存在?→ 用路径的二进制表示 - 完全二叉树的最后一个节点?→ 二分找最后一层最右


四、动态规划(10题)


34. 0-1背包问题

题目描述:n 件物品和容量为 W 的背包,第 i 件物品重量为 w[i],价值为 v[i]。每件物品只能用一次,求背包能装的最大价值。

思路分析:

  • dp[i][j] 表示前 i 件物品、容量为 j 时的最大价值
  • 转移方程:dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i]] + v[i])
  • 空间优化:一维数组,倒序遍历容量
Python
def knapsack(weights: list[int], values: list[int], capacity: int) -> int:
    n = len(weights)
    # 空间优化:一维 DP
    dp = [0] * (capacity + 1)

    for i in range(n):
        # 倒序遍历容量,避免重复使用同一物品
        for j in range(capacity, weights[i] - 1, -1):
            dp[j] = max(dp[j], dp[j - weights[i]] + values[i])

    return dp[capacity]

复杂度分析: - 时间复杂度:O(nW) - 空间复杂度:O(W),一维优化后

面试追问: - 完全背包(物品可重复使用)?→ 正序遍历容量 - 多重背包?→ 二进制优化 - 如何输出选了哪些物品?→ 回溯 dp 数组


35. 最长递增子序列(Longest Increasing Subsequence)

题目描述: 给定整数数组 nums,找到最长严格递增子序列的长度。

思路分析:

  • DP法: dp[i] 表示以 nums[i] 结尾的 LIS 长度,O(n²)
  • 贪心 + 二分查找: 维护一个有序数组 tails,用二分查找更新
Python
import bisect

def lengthOfLIS(nums: list[int]) -> int:
    # 贪心 + 二分查找 O(n log n)
    tails = []  # tails[i] = 长度为 i+1 的 LIS 的最小末尾元素

    for num in nums:
        pos = bisect.bisect_left(tails, num)
        if pos == len(tails):
            tails.append(num)  # 扩展 LIS
        else:
            tails[pos] = num   # 替换,使末尾更小

    return len(tails)

复杂度分析: - 时间复杂度:O(n log n) - 空间复杂度:O(n)

面试追问: - 如何输出最长递增子序列本身?→ 记录每个位置的前驱 - 最长不下降子序列?→ 用 bisect_right - 最长递增子序列的个数?→ DP + 计数


36. 编辑距离(Edit Distance)

题目描述: 给定两个单词 word1word2,计算将 word1 转换成 word2 所需的最少操作数(插入、删除、替换)。

思路分析:

dp[i][j] 表示 word1[0..i-1] 转换到 word2[0..j-1] 的最少操作数。

Python
def minDistance(word1: str, word2: str) -> int:
    m, n = len(word1), len(word2)
    # dp[i][j]: word1[:i] -> word2[:j] 的最少操作
    dp = [[0] * (n + 1) for _ in range(m + 1)]

    # 基础情况
    for i in range(m + 1):
        dp[i][0] = i  # 全删除
    for j in range(n + 1):
        dp[0][j] = j  # 全插入

    for i in range(1, m + 1):
        for j in range(1, n + 1):
            if word1[i - 1] == word2[j - 1]:
                dp[i][j] = dp[i - 1][j - 1]  # 字符相同,无需操作
            else:
                dp[i][j] = 1 + min(
                    dp[i - 1][j],      # 删除
                    dp[i][j - 1],      # 插入
                    dp[i - 1][j - 1]   # 替换
                )

    return dp[m][n]

复杂度分析: - 时间复杂度:O(mn) - 空间复杂度:O(mn),可优化到 O(min(m, n))

面试追问: - 只允许插入和删除?→ 转化为 LCS 问题 - 不同操作有不同代价?→ 修改转移方程中的权重 - 模糊匹配/拼写纠错?→ 编辑距离的实际应用


37. 买卖股票的最佳时机系列

题目描述: 给定股票每天价格数组,在不同约束下求最大利润。

思路分析:

  • 只买卖一次: 维护最低价,计算每天卖出的利润
  • 无限次买卖: 贪心,只要今天比昨天高就累加
  • 最多k次买卖: DP,状态为 (天数, 交易次数, 持有状态)
Python
# 买卖一次
def maxProfit_once(prices: list[int]) -> int:
    min_price = float('inf')
    max_profit = 0
    for price in prices:
        min_price = min(min_price, price)
        max_profit = max(max_profit, price - min_price)
    return max_profit

# 无限次买卖
def maxProfit_unlimited(prices: list[int]) -> int:
    return sum(max(0, prices[i] - prices[i - 1]) for i in range(1, len(prices)))

# 最多 k 次买卖
def maxProfit_k(k: int, prices: list[int]) -> int:
    n = len(prices)
    if n < 2:
        return 0

    # 如果 k >= n/2,等同于无限次
    if k >= n // 2:
        return sum(max(0, prices[i] - prices[i - 1]) for i in range(1, n))

    # dp[j][0/1]: 完成 j 次交易,不持有/持有 的最大利润
    dp = [[0, float('-inf')] for _ in range(k + 1)]

    for price in prices:
        for j in range(1, k + 1):
            dp[j][0] = max(dp[j][0], dp[j][1] + price)     # 卖出
            dp[j][1] = max(dp[j][1], dp[j - 1][0] - price) # 买入

    return dp[k][0]

复杂度分析: - 一次:O(n) 时间,O(1) 空间 - 无限次:O(n) 时间,O(1) 空间 - k次:O(nk) 时间,O(k) 空间

面试追问: - 含冷冻期?→ 卖出后一天不能买入 - 含手续费?→ 卖出时减去手续费 - 最多持有 m 股?→ 扩展状态维度


38. 零钱兑换(Coin Change)

题目描述: 给定不同面额的硬币 coins 和总金额 amount,计算凑成总金额所需的最少硬币数。

Python
def coinChange(coins: list[int], amount: int) -> int:
    # dp[i] 表示凑成金额 i 的最少硬币数
    dp = [float('inf')] * (amount + 1)
    dp[0] = 0

    for i in range(1, amount + 1):
        for coin in coins:
            if coin <= i:
                dp[i] = min(dp[i], dp[i - coin] + 1)

    return dp[amount] if dp[amount] != float('inf') else -1

复杂度分析: - 时间复杂度:O(amount × n),n 为硬币种类数 - 空间复杂度:O(amount)

面试追问: - 凑成 amount 的组合数?→ 完全背包变形 - 如果硬币数量有限?→ 多重背包 - BFS 方法?→ 最少步数 = 最短路径


39. 最长公共子序列(Longest Common Subsequence)

题目描述: 给定两个字符串 text1text2,返回它们的最长公共子序列的长度。

Python
def longestCommonSubsequence(text1: str, text2: str) -> int:
    m, n = len(text1), len(text2)
    dp = [[0] * (n + 1) for _ in range(m + 1)]

    for i in range(1, m + 1):
        for j in range(1, n + 1):
            if text1[i - 1] == text2[j - 1]:
                dp[i][j] = dp[i - 1][j - 1] + 1
            else:
                dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])

    return dp[m][n]

复杂度分析: - 时间复杂度:O(mn) - 空间复杂度:O(mn),可优化到 O(min(m, n))

面试追问: - 如何输出 LCS 本身?→ 回溯 dp 数组 - 最长公共子串(连续)?→ 修改转移方程 - 三个字符串的 LCS?→ 三维 DP


40. 正则表达式匹配(Regular Expression Matching)

题目描述: 实现支持 .* 的正则表达式匹配。. 匹配任意单字符,* 匹配零个或多个前面的元素。

Python
def isMatch(s: str, p: str) -> bool:
    m, n = len(s), len(p)
    # dp[i][j]: s[:i] 是否匹配 p[:j]
    dp = [[False] * (n + 1) for _ in range(m + 1)]
    dp[0][0] = True

    # 初始化:空字符串匹配 a*b*c* 形式
    for j in range(2, n + 1):
        if p[j - 1] == '*':
            dp[0][j] = dp[0][j - 2]

    for i in range(1, m + 1):
        for j in range(1, n + 1):
            if p[j - 1] == '*':
                # * 匹配零次 or 匹配一次以上
                dp[i][j] = dp[i][j - 2]  # 匹配零次
                if p[j - 2] == '.' or p[j - 2] == s[i - 1]:
                    dp[i][j] = dp[i][j] or dp[i - 1][j]  # 匹配一次以上
            elif p[j - 1] == '.' or p[j - 1] == s[i - 1]:
                dp[i][j] = dp[i - 1][j - 1]

    return dp[m][n]

复杂度分析: - 时间复杂度:O(mn) - 空间复杂度:O(mn)

面试追问: - 通配符匹配(?*)?→ * 匹配任意字符串,DP 类似 - NFA/DFA 实现?→ 自动机理论 - 如何处理反向引用 \1?→ 需要更复杂的 NFA


41. 打家劫舍(House Robber)

题目描述: 不能偷相邻房屋,求最大偷窃金额。

Python
def rob(nums: list[int]) -> int:
    if not nums:
        return 0
    if len(nums) <= 2:
        return max(nums)

    # 空间优化:只需前两个状态
    prev2, prev1 = nums[0], max(nums[0], nums[1])
    for i in range(2, len(nums)):
        curr = max(prev1, prev2 + nums[i])
        prev2, prev1 = prev1, curr

    return prev1

# 打家劫舍 II(环形)
def rob_circular(nums: list[int]) -> int:
    if len(nums) == 1:
        return nums[0]
    # 分两种情况:不偷第一间 or 不偷最后一间
    return max(rob(nums[1:]), rob(nums[:-1]))

复杂度分析: - 时间复杂度:O(n) - 空间复杂度:O(1)

面试追问: - 环形排列(打家劫舍II)?→ 分两种情况讨论 - 树形排列(打家劫舍III)?→ 树形 DP - 如果可以偷隔一间以上?→ 相当于无约束


42. 不同路径(Unique Paths)

题目描述: m × n 网格,从左上角到右下角,每次只能向下或向右移动一步,共有多少条不同路径。

Python
def uniquePaths(m: int, n: int) -> int:
    # 空间优化:一维 DP
    dp = [1] * n
    for i in range(1, m):
        for j in range(1, n):
            dp[j] += dp[j - 1]
    return dp[n - 1]

# 数学方法:组合数 C(m+n-2, m-1)
from math import comb
def uniquePaths_math(m: int, n: int) -> int:
    return comb(m + n - 2, m - 1)

复杂度分析: - DP:时间 O(mn),空间 O(n) - 数学:时间 O(min(m, n)),空间 O(1)

面试追问: - 有障碍物?→ 障碍位置 dp[j] = 0 - 最小路径和?→ dp[i][j] = min(上, 左) + grid[i][j] - 可以向四个方向走(不重复)?→ 回溯搜索


43. 最大子数组和(Maximum Subarray / Kadane's Algorithm)

题目描述: 找到连续子数组的最大和。

Python
def maxSubArray(nums: list[int]) -> int:
    max_sum = nums[0]
    current_sum = nums[0]

    for i in range(1, len(nums)):
        # 要么延续之前的子数组,要么从当前元素重新开始
        current_sum = max(nums[i], current_sum + nums[i])
        max_sum = max(max_sum, current_sum)

    return max_sum

复杂度分析: - 时间复杂度:O(n) - 空间复杂度:O(1)

面试追问: - 分治法?→ O(n log n),最大子数组要么全在左半、全在右半、或跨越中点 - 输出子数组的起止位置?→ 记录 start 和 end - 环形子数组最大和?→ max(正常最大和, 总和 - 正常最小和)


五、其他高频(7题)


44. 前 K 个高频元素(Top K Frequent Elements)

题目描述: 给定整数数组 nums 和整数 k,返回出现频率前 k 高的元素。

思路分析:

  • 排序法: O(n log n)
  • 最小堆: 维护大小为 k 的最小堆 O(n log k)
  • 桶排序: O(n)
Python
import heapq
from collections import Counter  # Counter计数器:统计元素出现次数

def topKFrequent(nums: list[int], k: int) -> list[int]:
    count = Counter(nums)
    # 最小堆,堆大小为 k
    return heapq.nlargest(k, count.keys(), key=count.get)

# 桶排序方法 O(n)
def topKFrequent_bucket(nums: list[int], k: int) -> list[int]:
    count = Counter(nums)
    n = len(nums)

    # 频率桶:bucket[i] 存储出现 i 次的元素
    bucket = [[] for _ in range(n + 1)]
    for num, freq in count.items():
        bucket[freq].append(num)

    result = []
    for freq in range(n, 0, -1):
        for num in bucket[freq]:
            result.append(num)
            if len(result) == k:
                return result
    return result

复杂度分析: - 堆方法:时间 O(n log k),空间 O(n) - 桶排序:时间 O(n),空间 O(n)

面试追问: - 快速选择算法?→ O(n) 平均时间 - 数据流场景?→ 维护堆 + 哈希表 - 前 K 个高频单词(有字母序要求)?→ 自定义排序


45. 数据流的中位数(Find Median from Data Stream)

题目描述: 设计一个支持 addNumfindMedian 的数据结构。

思路分析:

两个堆: 最大堆(存较小半部分) + 最小堆(存较大半部分),保持两堆大小平衡。

Python
import heapq

class MedianFinder:
    def __init__(self):
        self.small = []  # 最大堆(取反实现)
        self.large = []  # 最小堆

    def addNum(self, num: int) -> None:
        # 先加入最大堆
        heapq.heappush(self.small, -num)
        # 确保最大堆的最大值 ≤ 最小堆的最小值
        heapq.heappush(self.large, -heapq.heappop(self.small))
        # 平衡大小:最大堆可以多一个
        if len(self.large) > len(self.small):
            heapq.heappush(self.small, -heapq.heappop(self.large))

    def findMedian(self) -> float:
        if len(self.small) > len(self.large):
            return -self.small[0]
        return (-self.small[0] + self.large[0]) / 2

复杂度分析: - addNum:O(log n) - findMedian:O(1) - 空间复杂度:O(n)

面试追问: - 滑动窗口中位数?→ 需要支持删除操作,用有序集合 - 分布式场景?→ 分位数算法、t-digest - 如果数据在有限范围内?→ 桶计数


46. 岛屿数量(Number of Islands)

题目描述: 给定 m × n 二维网格('1' 为陆地,'0' 为水),计算岛屿数量。

Python
def numIslands(grid: list[list[str]]) -> int:
    if not grid:
        return 0

    m, n = len(grid), len(grid[0])
    count = 0

    def dfs(i, j):
        if i < 0 or i >= m or j < 0 or j >= n or grid[i][j] != '1':
            return
        grid[i][j] = '0'  # 标记已访问
        dfs(i + 1, j)
        dfs(i - 1, j)
        dfs(i, j + 1)
        dfs(i, j - 1)

    for i in range(m):
        for j in range(n):
            if grid[i][j] == '1':
                count += 1
                dfs(i, j)

    return count

复杂度分析: - 时间复杂度:O(mn) - 空间复杂度:O(mn),递归栈最坏情况

面试追问: - 用 BFS 实现?→ 队列遍历 - 用并查集实现?→ Union-Find - 岛屿最大面积?→ DFS 时计数 - 不修改原数组?→ 额外 visited 数组


题目描述: 给定 m × n 字符网格和一个单词,判断是否存在于网格中(上下左右相邻,不能重复使用)。

Python
def exist(board: list[list[str]], word: str) -> bool:
    m, n = len(board), len(board[0])

    def backtrack(i, j, k):
        if k == len(word):
            return True
        if i < 0 or i >= m or j < 0 or j >= n or board[i][j] != word[k]:
            return False

        # 标记已访问
        temp, board[i][j] = board[i][j], '#'

        found = (backtrack(i + 1, j, k + 1) or
                 backtrack(i - 1, j, k + 1) or
                 backtrack(i, j + 1, k + 1) or
                 backtrack(i, j - 1, k + 1))

        board[i][j] = temp  # 回溯
        return found

    for i in range(m):
        for j in range(n):
            if backtrack(i, j, 0):
                return True
    return False

复杂度分析: - 时间复杂度:O(m × n × 3^L),L 为单词长度 - 空间复杂度:O(L),递归深度

面试追问: - 搜索多个单词?→ 用 Trie 优化(单词搜索 II) - 允许对角线移动?→ 增加 4 个方向 - 环形网格?→ 取模处理边界


48. 全排列(Permutations)

题目描述: 给定不包含重复数字的数组 nums,返回所有可能的全排列。

Python
def permute(nums: list[int]) -> list[list[int]]:
    result = []

    def backtrack(path, remaining):
        if not remaining:
            result.append(path[:])  # 切片操作:[start:end:step]提取子序列
            return
        for i in range(len(remaining)):
            path.append(remaining[i])
            backtrack(path, remaining[:i] + remaining[i + 1:])
            path.pop()

    backtrack([], nums)
    return result

# 有重复元素的全排列
def permuteUnique(nums: list[int]) -> list[list[int]]:
    result = []
    nums.sort()
    used = [False] * len(nums)

    def backtrack(path):
        if len(path) == len(nums):
            result.append(path[:])
            return
        for i in range(len(nums)):
            if used[i]:
                continue
            # 剪枝:相同元素跳过(前一个相同元素未使用时跳过)
            if i > 0 and nums[i] == nums[i - 1] and not used[i - 1]:
                continue
            used[i] = True
            path.append(nums[i])
            backtrack(path)
            path.pop()
            used[i] = False

    backtrack([])
    return result

复杂度分析: - 时间复杂度:O(n × n!) - 空间复杂度:O(n),递归深度

面试追问: - 第 k 个排列?→ 数学方法逐位确定 - 下一个排列?→ 见题目 15 - 组合/子集问题?→ 类似回溯框架


49. 实现 Trie(前缀树)

题目描述: 实现 Trie 数据结构,支持 insertsearchstartsWith

Python
class TrieNode:
    def __init__(self):
        self.children = {}
        self.is_end = False

class Trie:
    def __init__(self):
        self.root = TrieNode()

    def insert(self, word: str) -> None:
        node = self.root
        for char in word:
            if char not in node.children:
                node.children[char] = TrieNode()
            node = node.children[char]
        node.is_end = True

    def search(self, word: str) -> bool:
        node = self._find(word)
        return node is not None and node.is_end

    def startsWith(self, prefix: str) -> bool:
        return self._find(prefix) is not None

    def _find(self, prefix: str) -> TrieNode:
        node = self.root
        for char in prefix:
            if char not in node.children:
                return None
            node = node.children[char]
        return node

复杂度分析: - insert/search/startsWith:O(m),m 为字符串长度 - 空间复杂度:O(字符总数 × 字符集大小)

面试追问: - 通配符搜索(. 匹配任意字符)?→ DFS 遍历所有子节点 - 自动补全?→ DFS 收集所有以前缀开头的单词 - 压缩 Trie(Radix Tree)?→ 合并只有一个子节点的路径


50. 最短路径 - Dijkstra 算法

题目描述: 给定加权有向图,求从源点到所有其他顶点的最短路径。

思路分析:

Dijkstra 算法: 贪心策略,每次选择距离最小的未访问节点,用它更新邻居的最短距离。用最小堆优化。

Python
import heapq
from collections import defaultdict  # defaultdict带默认值的字典,避免KeyError

def dijkstra(graph: dict, start: int, n: int) -> list[int]:
    """
    graph: 邻接表 {u: [(v, weight), ...]}
    start: 起始节点
    n: 节点数
    返回: 从 start 到每个节点的最短距离
    """
    dist = [float('inf')] * n
    dist[start] = 0
    heap = [(0, start)]  # (距离, 节点)

    while heap:
        d, u = heapq.heappop(heap)
        if d > dist[u]:
            continue  # 跳过过时的条目

        for v, weight in graph[u]:
            new_dist = dist[u] + weight
            if new_dist < dist[v]:
                dist[v] = new_dist
                heapq.heappush(heap, (new_dist, v))

    return dist

# 使用示例
# graph = defaultdict(list)
# graph[0] = [(1, 4), (2, 1)]
# graph[1] = [(3, 1)]
# graph[2] = [(1, 2), (3, 5)]
# distances = dijkstra(graph, 0, 4)

复杂度分析: - 时间复杂度:O((V + E) log V),优先队列实现 - 空间复杂度:O(V + E)

面试追问: - 有负权边怎么办?→ Bellman-Ford 算法 - 所有节点对的最短路径?→ Floyd-Warshall O(V³) - A* 搜索与 Dijkstra 的区别?→ A* 加入了启发式函数 - 无权图最短路径?→ BFS 就够了


六、面试总结与技巧

解题步骤(UMPIRE 法)

  1. U - Understand(理解题目): 复述题目,确认输入输出格式、边界条件
  2. M - Match(匹配模式): 将题目与已知的算法模式匹配(滑动窗口、双指针、DP 等)
  3. P - Plan(制定计划): 描述算法思路,讨论时间空间复杂度
  4. I - Implement(实现代码): 编写干净、有注释的代码
  5. R - Review(代码审查): 检查边界条件、特殊情况
  6. E - Evaluate(评估): 分析复杂度,讨论优化空间

高频算法模式速查

模式 适用场景 典型题目
双指针 有序数组、链表 两数之和、三数之和、接雨水
滑动窗口 连续子数组/子串 最长无重复子串、最小覆盖子串
快慢指针 链表环检测 环形链表、链表中点
BFS/DFS 图、树遍历 岛屿数量、层序遍历
回溯 排列组合、搜索 全排列、单词搜索、N皇后
动态规划 最优子结构 背包、LIS、编辑距离
贪心 局部最优→全局最优 买卖股票、合并区间
单调栈/队列 下一个更大元素 接雨水、滑动窗口最大值
二分查找 有序空间搜索 LIS(二分优化)、搜索旋转数组
并查集 连通性问题 岛屿数量、冗余连接
Top-K 问题 前K高频元素、合并K个链表
Trie 前缀匹配 实现Trie、单词搜索II

时间复杂度速查

Text Only
O(1) < O(log n) < O(n) < O(n log n) < O(n²) < O(2^n) < O(n!)
  • O(log n): 二分查找
  • O(n): 线性扫描、哈希表查找
  • O(n log n): 排序、堆操作
  • O(n²): 双层循环、简单 DP
  • O(2^n): 子集枚举
  • O(n!): 全排列

学习建议: 按类型刷题,每类先掌握模板,再练变种。面试时先说思路再写代码,注重沟通。