算法题种类与解题思路全面指南:基于LeetCode Hot 100与牛客Top 101

本文系统性地梳理了算法题的核心类型与解题思路,重点聚焦LeetCode Hot 100与牛客Top 101中的高频题目


一、算法学习的基础认知

1.1 复杂度分析基础

在深入学习具体算法之前,我们必须理解算法效率的衡量标准------时间复杂度和空间复杂度。这两个概念是评估算法优劣的核心指标。

时间复杂度的本质

时间复杂度并非指代码的实际运行时间,而是描述算法执行时间随数据规模增长的变化趋势,也称为渐进时间复杂度。用大O表示法记为T(n) = O(f(n)),其中n表示数据规模,f(n)表示代码执行次数与n的关系函数。

在计算时间复杂度时,我们遵循以下原则:

  • 只保留最高阶项
  • 忽略常数项和系数
  • 从内向外分析,从最深层开始

常见时间复杂度从优到劣排序:

复杂度 名称 说明 典型算法
O(1) 常数阶 执行时间不随n变化 数组按索引访问、哈希表查找
O(log n) 对数阶 每次减半问题规模 二分查找、平衡二叉树操作
O(n) 线性阶 执行次数与n成正比 线性查找、单层循环
O(n log n) 线性对数阶 高效排序的标准 快速排序、归并排序、堆排序
O(n²) 平方阶 双层嵌套循环 冒泡排序、插入排序、选择排序
O(n³) 立方阶 三层嵌套循环 某些动态规划问题
O(2ⁿ) 指数阶 问题规模指数增长 递归求解斐波那契数列、回溯法
O(n!) 阶乘阶 全排列问题 旅行商问题的暴力解法

空间复杂度分析

空间复杂度是算法在运行过程中临时占用存储空间大小的量度,同样使用大O表示法。我们主要关注算法需要分配的额外空间,不包括输入数据本身占用的空间。

递归算法的空间复杂度需要特别注意递归调用栈的深度。例如,递归计算斐波那契数列的空间复杂度为O(n),因为最多有n层递归调用栈。

1.2 LeetCode Hot 100与牛客Top 101题目分布概览

为了更有针对性地学习,我们首先了解这两个高频题库的题型分布特点:

LeetCode Hot 100题型统计
25% 12% 18% 15% 8% 7% 6% 5% 4% LeetCode Hot 100 题型分布 数组与字符串 链表 二叉树 动态规划 回溯与递归 图论与搜索 栈与队列 哈希表 其他

牛客Top 101题型统计

题型 题目数量 占比 难度分布
链表 15 14.9% 简单-中等
二叉树 17 16.8% 简单-困难
动态规划 16 15.8% 中等-困难
递归/回溯 10 9.9% 中等-困难
排序 9 8.9% 简单-中等
双指针 8 7.9% 简单-中等
二分查找 7 6.9% 中等
哈希 6 5.9% 简单-中等
堆/栈/队列 7 6.9% 中等
其他 6 5.9% 不定

从分布可以看出,数据结构类题目 (链表、树、栈/队列)和核心算法思想(动态规划、回溯、贪心)是高频考点的主体,这也将是本文重点讲解的内容。


二、基础数据结构类题目

2.1 数组与矩阵

核心特点

数组是最基础的数据结构,在内存中连续存储,支持O(1)时间的随机访问。

数组类题目通常考查对索引的灵活运用、边界条件处理、以及各种遍历技巧。

常见题型与解题思路

2.1.1 双指针技巧

双指针是数组题目中最常用的技巧之一,根据指针移动方式可分为:

对撞指针(两端向中间)

  • 适用场景: 有序数组查找、回文判断、容器问题
  • 经典题目:
    • 两数之和II(LeetCode 167)
    • 盛最多水的容器(LeetCode 11)
    • 三数之和(LeetCode 15)
python 复制代码
# 对撞指针模板
def two_pointers_converge(arr):
    left, right = 0, len(arr) - 1
    while left < right:
        # 根据条件移动指针
        if 满足某条件:
            left += 1
        else:
            right -= 1
    return result

快慢指针(同向移动)

  • 适用场景: 数组去重、移动元素、滑动窗口
  • 经典题目:
    • 删除有序数组中的重复项(LeetCode 26)
    • 移动零(LeetCode 283)
    • 颜色分类(LeetCode 75)
python 复制代码
# 快慢指针模板
def two_pointers_same_direction(arr):
    slow = 0
    for fast in range(len(arr)):
        if 满足保留条件:
            arr[slow] = arr[fast]
            slow += 1
    return slow  # slow即为新数组长度
2.1.2 前缀和技巧

前缀和是一种预处理技巧,通过构建前缀和数组,可以在O(1)时间内计算任意区间的和。

原理

plain 复制代码
原数组: [a₀, a₁, a₂, ..., aₙ]
前缀和: prefix[i] = a₀ + a₁ + ... + aᵢ₋₁
区间和: sum(i, j) = prefix[j+1] - prefix[i]

经典题目

  • 和为K的子数组(LeetCode 560): 前缀和 + 哈希表
  • 矩阵区域和检索(LeetCode 304): 二维前缀和
python 复制代码
# 一维前缀和模板
def prefix_sum(nums):
    n = len(nums)
    prefix = [0] * (n + 1)
    for i in range(n):
        prefix[i + 1] = prefix[i] + nums[i]
    return prefix

# 查询区间[i, j]的和
def range_sum(prefix, i, j):
    return prefix[j + 1] - prefix[i]
2.1.3 滑动窗口

滑动窗口是处理连续子数组/子串问题的利器,本质上是双指针的一种特殊应用。

适用条件

  • 问题涉及连续子数组/子串
  • 需要找满足某条件的最长/最短子数组
  • 具有单调性(扩大窗口使条件变差,缩小窗口使条件变好)

核心框架

python 复制代码
def sliding_window(s):
    left = 0
    window = {}  # 窗口内的数据
    result = 0
    
    for right in range(len(s)):
        # 扩大窗口
        c = s[right]
        window[c] = window.get(c, 0) + 1
        
        # 收缩窗口
        while 窗口需要收缩:
            d = s[left]
            window[d] -= 1
            left += 1
        
        # 更新结果
        result = max(result, right - left + 1)
    
    return result

经典题目

  • 无重复字符的最长子串(LeetCode 3)
  • 最小覆盖子串(LeetCode 76)
  • 找到字符串中所有字母异位词(LeetCode 438)
  • 长度最小的子数组(LeetCode 209)

时间复杂度分析

虽然有嵌套循环,但left和right都只会移动n次,因此时间复杂度是O(n)。

2.1.4 矩阵遍历

矩阵题目主要考查二维数组的遍历技巧和空间优化。

常见模式

  1. 螺旋遍历 : 按顺时针螺旋顺序访问矩阵元素
    • 螺旋矩阵(LeetCode 54)
    • 螺旋矩阵II(LeetCode 59)
  2. 对角线遍历 : 沿对角线方向访问
    • 对角线遍历(LeetCode 498)
  3. 原地修改 : 利用矩阵本身空间进行标记
    • 矩阵置零(LeetCode 73)
    • 旋转图像(LeetCode 48)
python 复制代码
# 螺旋遍历模板
def spiral_order(matrix):
    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 j in range(left, right + 1):
            result.append(matrix[top][j])
        top += 1
        
        # 从上到下
        for i in range(top, bottom + 1):
            result.append(matrix[i][right])
        right -= 1
        
        if top <= bottom:
            # 从右到左
            for j in range(right, left - 1, -1):
                result.append(matrix[bottom][j])
            bottom -= 1
        
        if left <= right:
            # 从下到上
            for i in range(bottom, top - 1, -1):
                result.append(matrix[i][left])
            left += 1
    
    return result

2.2 链表

核心特点

链表是一种线性数据结构,元素在内存中不连续存储,通过指针连接。

与数组相比,链表的优势在于插入和删除操作的O(1)时间复杂度(给定节点位置),劣势是不支持随机访问,查找需要O(n)时间。

链表题目的通用技巧

2.2.1 虚拟头节点(哨兵节点)

虚拟头节点是链表题目中最重要的技巧之一,可以统一处理头节点和其他节点,避免大量边界判断。

python 复制代码
# 标准模板
def process_linked_list(head):
    dummy = ListNode(0)  # 虚拟头节点
    dummy.next = head
    
    # 进行链表操作
    # ...
    
    return dummy.next  # 返回真正的头节点

应用场景

  • 删除节点(包括可能删除头节点)
  • 合并链表
  • 反转链表的部分或全部
2.2.2 快慢指针

快慢指针是链表题目中的另一大核心技巧,通过两个移动速度不同的指针解决多种问题。

典型应用

  1. 找中点(快指针走2步,慢指针走1步)
python 复制代码
def find_middle(head):
    slow = fast = head
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
    return slow  # slow指向中点
  1. 检测环(Floyd判圈算法)
python 复制代码
def has_cycle(head):
    slow = fast = head
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
        if slow == fast:
            return True
    return False
  1. 找环的入口
python 复制代码
def detect_cycle(head):
    slow = fast = head
    # 第一阶段:判断是否有环
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
        if slow == fast:
            break
    else:
        return None
    
    # 第二阶段:找入口
    slow = head
    while slow != fast:
        slow = slow.next
        fast = fast.next
    return slow

数学原理:设链表头到环入口距离为a,环入口到相遇点距离为b,环长为c,则:

  • 快指针走过: a + b + n*c
  • 慢指针走过: a + b
  • 由于快指针速度是慢指针2倍: a + b + n*c = 2(a + b)
  • 化简得: a = (n-1)*c + (c-b)
  • 含义: 从头节点到入口的距离 = 从相遇点继续走到入口的距离
  1. 删除倒数第N个节点
python 复制代码
def remove_nth_from_end(head, n):
    dummy = ListNode(0)
    dummy.next = head
    fast = slow = dummy
    
    # fast先走n+1步
    for _ in range(n + 1):
        fast = fast.next
    
    # fast和slow一起走,直到fast到达末尾
    while fast:
        fast = fast.next
        slow = slow.next
    
    # 删除slow.next
    slow.next = slow.next.next
    return dummy.next
2.2.3 链表反转

链表反转是面试中的经典题目,有多种变体。

1. 反转整个链表(迭代法)

python 复制代码
def reverse_list(head):
    prev = None
    curr = head
    while curr:
        next_temp = curr.next  # 保存下一个节点
        curr.next = prev       # 反转指针
        prev = curr            # prev前进
        curr = next_temp       # curr前进
    return prev

2. 反转整个链表(递归法)

python 复制代码
def reverse_list_recursive(head):
    if not head or not head.next:
        return head
    
    new_head = reverse_list_recursive(head.next)
    head.next.next = head  # 关键步骤
    head.next = None
    return new_head

3. 反转链表的一部分(LeetCode 92)

python 复制代码
def reverse_between(head, left, right):
    dummy = ListNode(0)
    dummy.next = head
    pre = dummy
    
    # 找到left前一个节点
    for _ in range(left - 1):
        pre = pre.next
    
    # 反转left到right部分
    curr = pre.next
    for _ in range(right - left):
        next_node = curr.next
        curr.next = next_node.next
        next_node.next = pre.next
        pre.next = next_node
    
    return dummy.next

4. K个一组翻转链表 (LeetCode 25,困难题)

这是链表反转的最难变体,需要结合多种技巧:

  • 计算链表长度,判断是否需要反转
  • 分组反转
  • 连接各个反转后的组
python 复制代码
def reverse_k_group(head, k):
    dummy = ListNode(0)
    dummy.next = head
    pre = dummy
    
    while True:
        # 检查剩余节点是否够k个
        tail = pre
        for _ in range(k):
            tail = tail.next
            if not tail:
                return dummy.next
        
        # 记录下一组的前驱节点
        next_group = tail.next
        
        # 反转当前组
        head, tail = reverse_one_group(pre.next, tail)
        
        # 连接
        pre.next = head
        tail.next = next_group
        pre = tail

def reverse_one_group(head, tail):
    prev = tail.next
    curr = head
    while prev != tail:
        next_temp = curr.next
        curr.next = prev
        prev = curr
        curr = next_temp
    return tail, head
2.2.4 合并链表

1. 合并两个有序链表(LeetCode 21)

python 复制代码
def merge_two_lists(l1, l2):
    dummy = ListNode(0)
    curr = dummy
    
    while l1 and l2:
        if l1.val < l2.val:
            curr.next = l1
            l1 = l1.next
        else:
            curr.next = l2
            l2 = l2.next
        curr = curr.next
    
    curr.next = l1 if l1 else l2
    return dummy.next

2. 合并K个有序链表 (LeetCode 23,困难题)

使用最小堆(优先队列)维护K个链表的当前最小节点。

python 复制代码
import heapq

def merge_k_lists(lists):
    dummy = ListNode(0)
    curr = dummy
    heap = []
    
    # 初始化堆
    for i, l in enumerate(lists):
        if l:
            heapq.heappush(heap, (l.val, i, l))
    
    # 不断取出最小值
    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(log K),需要进行N次操作。

2.2.5 链表排序

归并排序 (LeetCode 148)

链表排序的最佳选择是归并排序,因为:

  1. 不需要随机访问
  2. 空间复杂度可以做到O(1)
  3. 时间复杂度稳定O(n log n)
python 复制代码
def sort_list(head):
    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 = sort_list(head)
    right = sort_list(mid)
    
    # 合并
    return merge(left, right)

def merge(l1, l2):
    dummy = ListNode(0)
    curr = dummy
    while l1 and l2:
        if l1.val < l2.val:
            curr.next = l1
            l1 = l1.next
        else:
            curr.next = l2
            l2 = l2.next
        curr = curr.next
    curr.next = l1 if l1 else l2
    return dummy.next

2.3 栈与队列

核心特点

  • 栈: 后进先出(LIFO),主要操作为push和pop
  • 队列: 先进先出(FIFO),主要操作为enqueue和dequeue
  • 单调栈: 栈内元素保持单调性,用于解决下一个更大/更小元素问题
  • 单调队列: 队列内元素保持单调性,常用于滑动窗口最值问题
2.3.1 栈的经典应用

1. 括号匹配 (LeetCode 20)

这是栈最直接的应用,利用栈的LIFO特性匹配成对括号。

python 复制代码
def is_valid(s):
    stack = []
    mapping = {')': '(', '}': '{', ']': '['}
    
    for char in s:
        if char in mapping:
            if not stack or stack[-1] != mapping[char]:
                return False
            stack.pop()
        else:
            stack.append(char)
    
    return len(stack) == 0

2. 最小栈 (LeetCode 155)

要求实现一个栈,支持常数时间获取最小值。核心思路是用辅助栈记录每个状态的最小值。

python 复制代码
class MinStack:
    def __init__(self):
        self.stack = []
        self.min_stack = []
    
    def push(self, val):
        self.stack.append(val)
        if not self.min_stack or val <= self.min_stack[-1]:
            self.min_stack.append(val)
    
    def pop(self):
        if self.stack.pop() == self.min_stack[-1]:
            self.min_stack.pop()
    
    def top(self):
        return self.stack[-1]
    
    def get_min(self):
        return self.min_stack[-1]

3. 逆波兰表达式求值 (LeetCode 150)

栈天然适合处理后缀表达式。

python 复制代码
def eval_rpn(tokens):
    stack = []
    operators = {'+', '-', '*', '/'}
    
    for token in tokens:
        if token in operators:
            b = stack.pop()
            a = stack.pop()
            if token == '+':
                stack.append(a + b)
            elif token == '-':
                stack.append(a - b)
            elif token == '*':
                stack.append(a * b)
            else:
                stack.append(int(a / b))  # 向零截断
        else:
            stack.append(int(token))
    
    return stack[0]
2.3.2 单调栈

单调栈是一种特殊的栈,栈内元素保持单调递增或递减。主要用于解决"下一个更大元素"或"下一个更小元素"类问题。

核心思想

  • 单调递减栈: 从栈底到栈顶递减,用于找下一个更大元素
  • 单调递增栈: 从栈底到栈顶递增,用于找下一个更小元素

1. 下一个更大元素(LeetCode 496, 503, 739)

python 复制代码
# 每日温度(LeetCode 739)
def daily_temperatures(temperatures):
    n = len(temperatures)
    result = [0] * n
    stack = []  # 存储索引
    
    for i in range(n):
        while stack and temperatures[i] > temperatures[stack[-1]]:
            prev_index = stack.pop()
            result[prev_index] = i - prev_index
        stack.append(i)
    
    return result

工作原理

  1. 遍历数组,维护一个单调递减栈(存储索引)
  2. 当前元素大于栈顶元素时,说明找到了栈顶元素的"下一个更大元素"
  3. 不断弹出栈顶并记录答案,直到当前元素不再大于栈顶
  4. 将当前元素索引入栈

2. 接雨水 (LeetCode 42,困难题)

这是单调栈的经典应用,也可以用双指针解决。

python 复制代码
def trap(height):
    n = len(height)
    if n < 3:
        return 0
    
    result = 0
    stack = []  # 单调递减栈,存储索引
    
    for i in range(n):
        while stack and height[i] > height[stack[-1]]:
            top = stack.pop()
            if not stack:
                break
            
            left = stack[-1]
            width = i - left - 1
            h = min(height[left], height[i]) - height[top]
            result += width * h
        
        stack.append(i)
    
    return result

原理解析

单调栈解法按层计算雨水:

  • 栈维护可能形成积水的位置
  • 当遇到更高的柱子时,计算之前凹陷处的积水
  • 积水高度 = min(左柱子, 右柱子) - 凹陷处高度
  • 积水宽度 = 右柱子位置 - 左柱子位置 - 1

3. 柱状图中最大的矩形(LeetCode 84,困难题)

python 复制代码
def largest_rectangle_area(heights):
    stack = []
    max_area = 0
    heights = [0] + heights + [0]  # 前后加0,简化边界处理
    
    for i in range(len(heights)):
        while stack and heights[i] < heights[stack[-1]]:
            h = heights[stack.pop()]
            w = i - stack[-1] - 1
            max_area = max(max_area, h * w)
        stack.append(i)
    
    return max_area

原理

  • 维护单调递增栈
  • 当前柱子矮于栈顶时,说明栈顶柱子的"右边界"找到了
  • 栈顶下一个元素是"左边界"
  • 矩形宽度 = 右边界 - 左边界 - 1
2.3.3 队列的应用

1. 用栈实现队列 (LeetCode 232)

用两个栈实现队列的所有操作。

python 复制代码
class MyQueue:
    def __init__(self):
        self.stack_in = []   # 负责入队
        self.stack_out = []  # 负责出队
    
    def push(self, x):
        self.stack_in.append(x)
    
    def pop(self):
        if not self.stack_out:
            while self.stack_in:
                self.stack_out.append(self.stack_in.pop())
        return self.stack_out.pop()
    
    def peek(self):
        if not self.stack_out:
            while self.stack_in:
                self.stack_out.append(self.stack_in.pop())
        return self.stack_out[-1]
    
    def empty(self):
        return not self.stack_in and not self.stack_out

均摊时间复杂度分析

  • push: O(1)
  • pop: 看似O(n),但每个元素最多被移动两次,因此均摊O(1)

2. 滑动窗口最大值 (LeetCode 239,困难题)

使用单调队列(双端队列)维护窗口内的最大值。

python 复制代码
from collections import deque

def max_sliding_window(nums, k):
    dq = deque()  # 存储索引
    result = []
    
    for i in range(len(nums)):
        # 移除窗口外的元素
        if dq and dq[0] < i - k + 1:
            dq.popleft()
        
        # 维护单调递减队列
        while dq and nums[dq[-1]] < nums[i]:
            dq.pop()
        
        dq.append(i)
        
        # 窗口形成后记录最大值
        if i >= k - 1:
            result.append(nums[dq[0]])
    
    return result

核心思想

  • 队列保持单调递减(队首最大)
  • 队首元素就是窗口的最大值
  • 新元素入队时,从队尾移除所有比它小的元素(因为它们不可能是答案)
  • 时间复杂度: O(n),每个元素最多入队出队各一次

2.4 哈希表

核心特点

哈希表通过哈希函数将键映射到数组索引,实现O(1)平均时间的查找、插入和删除。

是用空间换时间的典型数据结构。

常见应用场景

  1. 快速查找: 判断元素是否存在
  2. 计数统计: 统计元素出现次数
  3. 去重: 利用哈希表key的唯一性
  4. 建立映射关系: 存储key-value对应关系
2.4.1 经典哈希表题目

1. 两数之和 (LeetCode 1)

这是LeetCode的第一题,也是哈希表应用的经典例子。

python 复制代码
def two_sum(nums, target):
    hash_map = {}
    for i, num in enumerate(nums):
        complement = target - num
        if complement in hash_map:
            return [hash_map[complement], i]
        hash_map[num] = i
    return []

优化思路

  • 暴力解法: O(n²),两层循环
  • 哈希表: O(n),用空间换时间,只需遍历一次

2. 字母异位词分组 (LeetCode 49)

核心是找到一个标准化的key来表示异位词。

python 复制代码
from collections import defaultdict

def group_anagrams(strs):
    hash_map = defaultdict(list)
    for s in strs:
        # 排序后的字符串作为key
        key = ''.join(sorted(s))
        hash_map[key].append(s)
    return list(hash_map.values())

另一种key的构造方法(更高效)

python 复制代码
def group_anagrams(strs):
    hash_map = defaultdict(list)
    for s in strs:
        # 用字符计数作为key
        count = [0] * 26
        for c in s:
            count[ord(c) - ord('a')] += 1
        hash_map[tuple(count)].append(s)
    return list(hash_map.values())

3. 最长连续序列 (LeetCode 128)

要求O(n)时间复杂度,因此不能排序。

python 复制代码
def longest_consecutive(nums):
    num_set = set(nums)
    max_length = 0
    
    for num in num_set:
        # 只从序列的起点开始计数
        if num - 1 not in num_set:
            current_num = num
            current_length = 1
            
            while current_num + 1 in num_set:
                current_num += 1
                current_length += 1
            
            max_length = max(max_length, current_length)
    
    return max_length

关键优化

  • 只从序列起点开始计数(num-1不在集合中)
  • 避免重复计算
  • 每个数字最多被访问两次,时间复杂度O(n)

4. LRU缓存 (LeetCode 146,中等偏难)

要求实现一个固定容量的缓存,支持O(1)时间的get和put操作,并在容量满时删除最久未使用的项。

python 复制代码
class Node:
    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):
        self.cache = {}  # key -> Node
        self.capacity = capacity
        # 虚拟头尾节点
        self.head = Node()
        self.tail = Node()
        self.head.next = self.tail
        self.tail.prev = self.head
    
    def get(self, key):
        if key not in self.cache:
            return -1
        node = self.cache[key]
        self._move_to_head(node)
        return node.value
    
    def put(self, key, value):
        if key in self.cache:
            node = self.cache[key]
            node.value = value
            self._move_to_head(node)
        else:
            node = Node(key, value)
            self.cache[key] = node
            self._add_to_head(node)
            if len(self.cache) > self.capacity:
                removed = self._remove_tail()
                del self.cache[removed.key]
    
    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)查找
  • 双向链表: O(1)插入和删除
  • 两者结合: 实现所有操作O(1)

三、树相关算法

3.1 二叉树基础

二叉树是最重要的非线性数据结构之一,每个节点最多有两个子节点。二叉树题目通常考查递归、遍历、路径问题等。

二叉树节点定义

python 复制代码
class TreeNode:
    def __init__(self, val=0, left=None, right=None):
        self.val = val
        self.left = left
        self.right = right
3.1.1 二叉树遍历

1. 前序遍历(根-左-右)

递归实现:

python 复制代码
def preorder_traversal(root):
    result = []
    def dfs(node):
        if not node:
            return
        result.append(node.val)  # 根
        dfs(node.left)           # 左
        dfs(node.right)          # 右
    dfs(root)
    return result

迭代实现(用栈):

python 复制代码
def preorder_traversal_iterative(root):
    if not root:
        return []
    
    result = []
    stack = [root]
    
    while stack:
        node = stack.pop()
        result.append(node.val)
        # 先右后左,因为栈是LIFO
        if node.right:
            stack.append(node.right)
        if node.left:
            stack.append(node.left)
    
    return result

2. 中序遍历(左-根-右)

递归实现:

python 复制代码
def inorder_traversal(root):
    result = []
    def dfs(node):
        if not node:
            return
        dfs(node.left)          # 左
        result.append(node.val) # 根
        dfs(node.right)         # 右
    dfs(root)
    return result

迭代实现:

python 复制代码
def inorder_traversal_iterative(root):
    result = []
    stack = []
    curr = root
    
    while curr or stack:
        # 一直向左走到底
        while curr:
            stack.append(curr)
            curr = curr.left
        
        # 处理栈顶节点
        curr = stack.pop()
        result.append(curr.val)
        
        # 转向右子树
        curr = curr.right
    
    return result

重要性质

  • 二叉搜索树的中序遍历结果是有序的
  • 这个性质是BST相关题目的关键

3. 后序遍历(左-右-根)

递归实现:

python 复制代码
def postorder_traversal(root):
    result = []
    def dfs(node):
        if not node:
            return
        dfs(node.left)          # 左
        dfs(node.right)         # 右
        result.append(node.val) # 根
    dfs(root)
    return result

迭代实现(技巧:前序遍历的变体):

python 复制代码
def postorder_traversal_iterative(root):
    if not root:
        return []
    
    result = []
    stack = [root]
    
    while stack:
        node = stack.pop()
        result.append(node.val)
        # 先左后右(与前序相反)
        if node.left:
            stack.append(node.left)
        if node.right:
            stack.append(node.right)
    
    return result[::-1]  # 反转得到后序

4. 层序遍历(BFS)

层序遍历是按层访问节点,需要用队列实现。

python 复制代码
from collections import deque

def level_order(root):
    if not root:
        return []
    
    result = []
    queue = deque([root])
    
    while queue:
        level_size = len(queue)
        level_nodes = []
        
        for _ in range(level_size):
            node = queue.popleft()
            level_nodes.append(node.val)
            
            if node.left:
                queue.append(node.left)
            if node.right:
                queue.append(node.right)
        
        result.append(level_nodes)
    
    return result

变体题目

  • 二叉树的右视图(LeetCode 199): 每层最右边的节点
  • 二叉树的锯齿形层序遍历(LeetCode 103): 奇数层正序,偶数层逆序
  • 二叉树的层平均值(LeetCode 637): 计算每层平均值
3.1.2 二叉树的深度和高度

1. 最大深度(LeetCode 104)

递归解法:

python 复制代码
def max_depth(root):
    if not root:
        return 0
    return 1 + max(max_depth(root.left), max_depth(root.right))

迭代解法(BFS):

python 复制代码
def max_depth_iterative(root):
    if not root:
        return 0
    
    depth = 0
    queue = deque([root])
    
    while queue:
        depth += 1
        for _ in range(len(queue)):
            node = queue.popleft()
            if node.left:
                queue.append(node.left)
            if node.right:
                queue.append(node.right)
    
    return depth

2. 最小深度 (LeetCode 111)

注意最小深度的定义:从根节点到最近叶子节点的路径长度。

python 复制代码
def min_depth(root):
    if not root:
        return 0
    
    # 如果只有一个子树,返回该子树的最小深度
    if not root.left:
        return 1 + min_depth(root.right)
    if not root.right:
        return 1 + min_depth(root.left)
    
    # 两个子树都存在
    return 1 + min(min_depth(root.left), min_depth(root.right))

3. 平衡二叉树 (LeetCode 110)

判断一棵树是否是平衡二叉树(任意节点的左右子树高度差不超过1)。

python 复制代码
def is_balanced(root):
    def height(node):
        if not node:
            return 0
        
        left_height = height(node.left)
        if left_height == -1:
            return -1
        
        right_height = height(node.right)
        if right_height == -1:
            return -1
        
        if abs(left_height - right_height) > 1:
            return -1
        
        return 1 + max(left_height, right_height)
    
    return height(root) != -1

优化技巧: 用-1表示不平衡,避免重复计算高度。

3.1.3 二叉树路径问题

1. 路径总和 (LeetCode 112)

判断是否存在根到叶子的路径,使得路径上所有节点值之和等于目标值。

python 复制代码
def has_path_sum(root, target_sum):
    if not root:
        return False
    
    if not root.left and not root.right:
        return root.val == target_sum
    
    return (has_path_sum(root.left, target_sum - root.val) or
            has_path_sum(root.right, target_sum - root.val))

2. 路径总和II (LeetCode 113)

找出所有满足条件的路径。

python 复制代码
def path_sum(root, target_sum):
    result = []
    
    def dfs(node, path, remain):
        if not node:
            return
        
        path.append(node.val)
        
        if not node.left and not node.right and remain == node.val:
            result.append(path[:])  # 复制路径
        
        dfs(node.left, path, remain - node.val)
        dfs(node.right, path, remain - node.val)
        
        path.pop()  # 回溯
    
    dfs(root, [], target_sum)
    return result

3. 路径总和III (LeetCode 437,中等偏难)

路径不一定从根开始,也不一定到叶子结束。

python 复制代码
def path_sum_iii(root, target_sum):
    def count_paths(node, current_sum):
        if not node:
            return 0
        
        current_sum += node.val
        count = 0
        
        # 以当前节点结束的路径数
        if current_sum == target_sum:
            count += 1
        
        # 继续向下搜索
        count += count_paths(node.left, current_sum)
        count += count_paths(node.right, current_sum)
        
        return count
    
    if not root:
        return 0
    
    # 从当前节点开始的路径数
    paths_from_root = count_paths(root, 0)
    
    # 从左右子树开始的路径数
    paths_from_left = path_sum_iii(root.left, target_sum)
    paths_from_right = path_sum_iii(root.right, target_sum)
    
    return paths_from_root + paths_from_left + paths_from_right

优化: 使用前缀和+哈希表,时间复杂度从O(n²)降到O(n)。

python 复制代码
def path_sum_iii_optimized(root, target_sum):
    prefix_sum_count = {0: 1}  # 前缀和 -> 出现次数
    
    def dfs(node, current_sum):
        if not node:
            return 0
        
        current_sum += node.val
        count = prefix_sum_count.get(current_sum - target_sum, 0)
        
        prefix_sum_count[current_sum] = prefix_sum_count.get(current_sum, 0) + 1
        
        count += dfs(node.left, current_sum)
        count += dfs(node.right, current_sum)
        
        prefix_sum_count[current_sum] -= 1  # 回溯
        
        return count
    
    return dfs(root, 0)

4. 二叉树的最大路径和 (LeetCode 124,困难题)

路径可以从任意节点开始到任意节点结束。

python 复制代码
def max_path_sum(root):
    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)
        
        # 当前节点作为路径顶点的最大路径和
        path_sum = node.val + left_gain + right_gain
        max_sum = max(max_sum, path_sum)
        
        # 返回当前节点对父节点的贡献
        return node.val + max(left_gain, right_gain)
    
    max_gain(root)
    return max_sum

关键点

  • 递归函数返回的是单侧路径的最大值
  • 在递归过程中更新全局最大值(考虑经过当前节点的完整路径)

3.2 二叉搜索树(BST)

定义: 二叉搜索树是一种特殊的二叉树,满足:

  • 左子树所有节点值 < 根节点值
  • 右子树所有节点值 > 根节点值
  • 左右子树也分别是二叉搜索树

重要性质

  • BST的中序遍历结果是升序的
  • 这个性质是解决BST问题的关键
3.2.1 BST的验证和搜索

1. 验证二叉搜索树(LeetCode 98)

错误的做法:

python 复制代码
# 错误! 只比较了父子节点
def is_valid_bst_wrong(root):
    if not root:
        return True
    if root.left and root.left.val >= root.val:
        return False
    if root.right and root.right.val <= root.val:
        return False
    return is_valid_bst_wrong(root.left) and is_valid_bst_wrong(root.right)

正确的做法(维护取值范围):

python 复制代码
def is_valid_bst(root):
    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'))

利用中序遍历:

python 复制代码
def is_valid_bst_inorder(root):
    prev = float('-inf')
    stack = []
    curr = root
    
    while curr or stack:
        while curr:
            stack.append(curr)
            curr = curr.left
        
        curr = stack.pop()
        if curr.val <= prev:
            return False
        prev = curr.val
        curr = curr.right
    
    return True

2. 二叉搜索树中的搜索(LeetCode 700)

python 复制代码
def search_bst(root, val):
    if not root or root.val == val:
        return root
    
    if val < root.val:
        return search_bst(root.left, val)
    else:
        return search_bst(root.right, val)

迭代版本(空间复杂度O(1)):

python 复制代码
def search_bst_iterative(root, val):
    while root:
        if val == root.val:
            return root
        elif val < root.val:
            root = root.left
        else:
            root = root.right
    return None

3. 二叉搜索树中第K小的元素(LeetCode 230)

利用BST中序遍历的性质:

python 复制代码
def kth_smallest(root, k):
    stack = []
    curr = root
    count = 0
    
    while curr or stack:
        while curr:
            stack.append(curr)
            curr = curr.left
        
        curr = stack.pop()
        count += 1
        if count == k:
            return curr.val
        curr = curr.right
    
    return -1
3.2.2 BST的修改操作

1. 插入节点(LeetCode 701)

python 复制代码
def insert_into_bst(root, val):
    if not root:
        return TreeNode(val)
    
    if val < root.val:
        root.left = insert_into_bst(root.left, val)
    else:
        root.right = insert_into_bst(root.right, val)
    
    return root

2. 删除节点(LeetCode 450,中等偏难)

删除节点是BST操作中最复杂的,需要分三种情况:

  1. 删除叶子节点: 直接删除
  2. 删除只有一个子节点的节点: 用子节点替代
  3. 删除有两个子节点的节点: 用后继节点(右子树最小值)或前驱节点(左子树最大值)替代
python 复制代码
def delete_node(root, key):
    if not root:
        return None
    
    if key < root.val:
        root.left = delete_node(root.left, key)
    elif key > root.val:
        root.right = delete_node(root.right, key)
    else:
        # 找到要删除的节点
        if not root.left:
            return root.right
        if not root.right:
            return root.left
        
        # 有两个子节点:用后继节点替代
        successor = find_min(root.right)
        root.val = successor.val
        root.right = delete_node(root.right, successor.val)
    
    return root

def find_min(node):
    while node.left:
        node = node.left
    return node
3.2.3 BST的构造

1. 有序数组转BST (LeetCode 108)

要构造高度平衡的BST,选择中间元素作为根。

python 复制代码
def sorted_array_to_bst(nums):
    if not nums:
        return None
    
    mid = len(nums) // 2
    root = TreeNode(nums[mid])
    root.left = sorted_array_to_bst(nums[:mid])
    root.right = sorted_array_to_bst(nums[mid+1:])
    
    return root

2. 前序和中序遍历构造二叉树(LeetCode 105,中等偏难)

python 复制代码
def build_tree(preorder, inorder):
    if not preorder:
        return None
    
    root_val = preorder[0]
    root = TreeNode(root_val)
    
    # 在中序遍历中找到根节点位置
    root_idx = inorder.index(root_val)
    
    # 递归构造左右子树
    root.left = build_tree(preorder[1:root_idx+1], inorder[:root_idx])
    root.right = build_tree(preorder[root_idx+1:], inorder[root_idx+1:])
    
    return root

优化: 使用哈希表存储中序遍历的索引,避免重复查找。

python 复制代码
def build_tree_optimized(preorder, inorder):
    inorder_map = {val: idx for idx, val in enumerate(inorder)}
    
    def helper(pre_start, pre_end, in_start, in_end):
        if pre_start > pre_end:
            return None
        
        root_val = preorder[pre_start]
        root = TreeNode(root_val)
        root_idx = inorder_map[root_val]
        
        left_size = root_idx - in_start
        
        root.left = helper(pre_start + 1, pre_start + left_size,
                          in_start, root_idx - 1)
        root.right = helper(pre_start + left_size + 1, pre_end,
                           root_idx + 1, in_end)
        
        return root
    
    return helper(0, len(preorder) - 1, 0, len(inorder) - 1)

3.3 二叉树的高级操作

3.3.1 二叉树的序列化与反序列化

序列化 (LeetCode 297,困难题)

将树结构转换为字符串,并能从字符串恢复树结构。

前序遍历方式:

python 复制代码
class Codec:
    def serialize(self, root):
        """将树序列化为字符串"""
        if not root:
            return "null"
        
        left = self.serialize(root.left)
        right = self.serialize(root.right)
        
        return f"{root.val},{left},{right}"
    
    def deserialize(self, data):
        """从字符串反序列化树"""
        def helper(values):
            val = next(values)
            if val == "null":
                return None
            
            node = TreeNode(int(val))
            node.left = helper(values)
            node.right = helper(values)
            return node
        
        values = iter(data.split(','))
        return helper(values)

层序遍历方式:

python 复制代码
class Codec:
    def serialize(self, root):
        if not root:
            return ""
        
        result = []
        queue = deque([root])
        
        while queue:
            node = queue.popleft()
            if node:
                result.append(str(node.val))
                queue.append(node.left)
                queue.append(node.right)
            else:
                result.append("null")
        
        return ",".join(result)
    
    def deserialize(self, data):
        if not data:
            return None
        
        values = data.split(',')
        root = TreeNode(int(values[0]))
        queue = deque([root])
        i = 1
        
        while queue:
            node = queue.popleft()
            
            if values[i] != "null":
                node.left = TreeNode(int(values[i]))
                queue.append(node.left)
            i += 1
            
            if values[i] != "null":
                node.right = TreeNode(int(values[i]))
                queue.append(node.right)
            i += 1
        
        return root
3.3.2 最近公共祖先

1. 二叉树的最近公共祖先(LeetCode 236,中等偏难)

python 复制代码
def lowest_common_ancestor(root, p, q):
    if not root or root == p or root == q:
        return root
    
    left = lowest_common_ancestor(root.left, p, q)
    right = lowest_common_ancestor(root.right, p, q)
    
    # p和q分别在左右子树
    if left and right:
        return root
    
    # p和q都在左子树或都在右子树
    return left if left else right

核心思想

  • 如果p和q分别在root的左右子树,root就是LCA
  • 如果都在左子树,LCA也在左子树
  • 如果都在右子树,LCA也在右子树

2. 二叉搜索树的最近公共祖先 (LeetCode 235)

利用BST的性质可以简化算法。

python 复制代码
def lowest_common_ancestor_bst(root, p, q):
    while root:
        # p和q都在左子树
        if p.val < root.val and q.val < root.val:
            root = root.left
        # p和q都在右子树
        elif p.val > root.val and q.val > root.val:
            root = root.right
        # p和q分别在两侧,或其中一个就是root
        else:
            return root
    return None

四、搜索与图论

4.1 深度优先搜索(DFS)

核心思想

DFS采用"一条路走到黑"的策略,沿着一条路径尽可能深地搜索,直到无法继续,然后回溯到上一个节点,尝试其他路径。

实现方式

  1. 递归(隐式栈)
  2. 显式栈

经典应用场景

  • 路径搜索
  • 连通性问题
  • 拓扑排序
  • 岛屿问题
4.1.1 岛屿类问题

岛屿问题是DFS/BFS的经典应用,也是LeetCode和牛客的高频题型。

1. 岛屿数量(LeetCode 200)

python 复制代码
def num_islands(grid):
    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
        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

2. 岛屿的最大面积(LeetCode 695)

python 复制代码
def max_area_of_island(grid):
    m, n = len(grid), len(grid[0])
    max_area = 0
    
    def dfs(i, j):
        if i < 0 or i >= m or j < 0 or j >= n or grid[i][j] != 1:
            return 0
        
        grid[i][j] = 0
        area = 1
        
        area += dfs(i + 1, j)
        area += dfs(i - 1, j)
        area += dfs(i, j + 1)
        area += dfs(i, j - 1)
        
        return area
    
    for i in range(m):
        for j in range(n):
            if grid[i][j] == 1:
                max_area = max(max_area, dfs(i, j))
    
    return max_area

3. 被围绕的区域 (LeetCode 130,中等偏难)

核心思路:从边界的'O'开始DFS,标记所有不会被包围的'O'。

python 复制代码
def solve(board):
    if not board:
        return
    
    m, n = len(board), len(board[0])
    
    def dfs(i, j):
        if i < 0 or i >= m or j < 0 or j >= n or board[i][j] != 'O':
            return
        
        board[i][j] = 'T'  # 临时标记
        dfs(i + 1, j)
        dfs(i - 1, j)
        dfs(i, j + 1)
        dfs(i, j - 1)
    
    # 从边界DFS
    for i in range(m):
        dfs(i, 0)
        dfs(i, n - 1)
    for j in range(n):
        dfs(0, j)
        dfs(m - 1, j)
    
    # 更新棋盘
    for i in range(m):
        for j in range(n):
            if board[i][j] == 'T':
                board[i][j] = 'O'
            elif board[i][j] == 'O':
                board[i][j] = 'X'
4.1.2 单词搜索

单词搜索 (LeetCode 79)

在二维网格中搜索单词,可以上下左右移动,但不能重复使用同一个格子。

python 复制代码
def exist(board, word):
    m, n = len(board), len(board[0])
    
    def dfs(i, j, k):
        # k是word中当前要匹配的字符索引
        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 = (dfs(i + 1, j, k + 1) or
                dfs(i - 1, j, k + 1) or
                dfs(i, j + 1, k + 1) or
                dfs(i, j - 1, k + 1))
        
        # 回溯
        board[i][j] = temp
        
        return found
    
    for i in range(m):
        for j in range(n):
            if dfs(i, j, 0):
                return True
    
    return False

关键点

  • 原地修改board来标记访问状态,避免额外空间
  • 回溯时恢复现场
  • 剪枝:如果当前字符不匹配就立即返回

4.2 广度优先搜索(BFS)

核心思想

BFS采用"一层一层"的搜索策略,先访问距离起点近的节点,再访问远的节点。

实现方式

使用队列

适用场景

  • 最短路径问题(无权图)
  • 层次遍历
  • 连通性判断
4.2.1 BFS求最短路径

1. 二进制矩阵中的最短路径(LeetCode 1091)

python 复制代码
from collections import deque

def shortest_path_binary_matrix(grid):
    n = len(grid)
    if grid[0][0] == 1 or grid[n-1][n-1] == 1:
        return -1
    
    if n == 1:
        return 1
    
    queue = deque([(0, 0, 1)])  # (row, col, distance)
    grid[0][0] = 1  # 标记为已访问
    
    directions = [(-1,-1),(-1,0),(-1,1),(0,-1),(0,1),(1,-1),(1,0),(1,1)]
    
    while queue:
        row, col, dist = queue.popleft()
        
        for dr, dc in directions:
            r, c = row + dr, col + dc
            
            if r == n-1 and c == n-1:
                return dist + 1
            
            if 0 <= r < n and 0 <= c < n and grid[r][c] == 0:
                grid[r][c] = 1
                queue.append((r, c, dist + 1))
    
    return -1

2. 单词接龙 (LeetCode 127,困难题)

从beginWord变换到endWord,每次只能改变一个字母,且中间词必须在字典中。

python 复制代码
def ladder_length(begin_word, end_word, word_list):
    if end_word not in word_list:
        return 0
    
    word_set = set(word_list)
    queue = deque([(begin_word, 1)])
    
    while queue:
        word, length = queue.popleft()
        
        if word == end_word:
            return length
        
        # 尝试改变每个位置的字母
        for i in range(len(word)):
            for c in 'abcdefghijklmnopqrstuvwxyz':
                next_word = word[:i] + c + word[i+1:]
                
                if next_word in word_set:
                    word_set.remove(next_word)
                    queue.append((next_word, length + 1))
    
    return 0

优化:双向BFS

从起点和终点同时BFS,可以大幅减少搜索空间。

python 复制代码
def ladder_length_bidirectional(begin_word, end_word, word_list):
    if end_word not in word_list:
        return 0
    
    word_set = set(word_list)
    begin_set = {begin_word}
    end_set = {end_word}
    length = 1
    
    while begin_set and end_set:
        # 总是从较小的集合开始扩展
        if len(begin_set) > len(end_set):
            begin_set, end_set = end_set, begin_set
        
        next_set = set()
        for word in begin_set:
            for i in range(len(word)):
                for c in 'abcdefghijklmnopqrstuvwxyz':
                    next_word = word[:i] + c + word[i+1:]
                    
                    if next_word in end_set:
                        return length + 1
                    
                    if next_word in word_set:
                        word_set.remove(next_word)
                        next_set.add(next_word)
        
        begin_set = next_set
        length += 1
    
    return 0

4.3 拓扑排序

定义: 对有向无环图(DAG)的所有顶点进行线性排序,使得对于任何有向边u->v,u在排序中都在v之前。

应用场景

  • 课程安排
  • 任务调度
  • 编译顺序

算法思路(Kahn算法)

  1. 统计每个节点的入度
  2. 将入度为0的节点加入队列
  3. 从队列中取出节点,将其指向的节点入度-1
  4. 重复步骤2-3,直到队列为空

课程表(LeetCode 207)

python 复制代码
from collections import deque, defaultdict

def can_finish(num_courses, prerequisites):
    # 构建图和入度表
    graph = defaultdict(list)
    in_degree = [0] * num_courses
    
    for course, prereq in prerequisites:
        graph[prereq].append(course)
        in_degree[course] += 1
    
    # BFS
    queue = deque([i for i in range(num_courses) if in_degree[i] == 0])
    count = 0
    
    while queue:
        course = queue.popleft()
        count += 1
        
        for next_course in graph[course]:
            in_degree[next_course] -= 1
            if in_degree[next_course] == 0:
                queue.append(next_course)
    
    return count == num_courses

课程表II (LeetCode 210)

在判断能否完成的基础上,返回一个可行的课程顺序。

python 复制代码
def find_order(num_courses, prerequisites):
    graph = defaultdict(list)
    in_degree = [0] * num_courses
    
    for course, prereq in prerequisites:
        graph[prereq].append(course)
        in_degree[course] += 1
    
    queue = deque([i for i in range(num_courses) if in_degree[i] == 0])
    result = []
    
    while queue:
        course = queue.popleft()
        result.append(course)
        
        for next_course in graph[course]:
            in_degree[next_course] -= 1
            if in_degree[next_course] == 0:
                queue.append(next_course)
    
    return result if len(result) == num_courses else []

五、回溯算法

5.1 回溯算法核心思想

定义: 回溯算法是一种通过探索所有可能的候选解来找出所有解的算法。如果候选解不可行,则放弃该候选解,回溯到上一步,尝试其他可能的解。

本质: 暴力搜索 + 剪枝

适用场景

  • 求所有解(组合、排列、子集)
  • 求可行解(N皇后、数独)
  • 求最优解(结合剪枝)

回溯算法模板

python 复制代码
def backtrack(路径, 选择列表):
    if 满足结束条件:
        result.add(路径)
        return
    
    for 选择 in 选择列表:
        做选择
        backtrack(路径, 选择列表)
        撤销选择  # 回溯

5.2 组合问题

1. 组合 (LeetCode 77)

从1到n中选k个数的所有组合。

python 复制代码
def combine(n, k):
    result = []
    
    def backtrack(start, path):
        # 剪枝:剩余元素不足
        if len(path) + (n - start + 1) < k:
            return
        
        if len(path) == k:
            result.append(path[:])
            return
        
        for i in range(start, n + 1):
            path.append(i)
            backtrack(i + 1, path)
            path.pop()
    
    backtrack(1, [])
    return result

关键点

  • start参数避免重复组合
  • 剪枝提高效率

2. 组合总和 (LeetCode 39)

给定数组(无重复元素),找出和为target的所有组合,同一个数可以重复使用。

python 复制代码
def combination_sum(candidates, target):
    result = []
    candidates.sort()  # 排序便于剪枝
    
    def backtrack(start, path, remain):
        if remain == 0:
            result.append(path[:])
            return
        
        for i in range(start, len(candidates)):
            if candidates[i] > remain:
                break  # 剪枝
            
            path.append(candidates[i])
            backtrack(i, path, remain - candidates[i])  # 注意是i不是i+1
            path.pop()
    
    backtrack(0, [], target)
    return result

3. 组合总和II (LeetCode 40)

数组有重复元素,每个元素只能使用一次,但结果不能有重复组合。

python 复制代码
def combination_sum2(candidates, target):
    result = []
    candidates.sort()
    
    def backtrack(start, path, remain):
        if remain == 0:
            result.append(path[:])
            return
        
        for i in range(start, len(candidates)):
            if candidates[i] > remain:
                break
            
            # 去重:同一层不能使用相同的元素
            if i > start and candidates[i] == candidates[i-1]:
                continue
            
            path.append(candidates[i])
            backtrack(i + 1, path, remain - candidates[i])  # i+1表示不重复使用
            path.pop()
    
    backtrack(0, [], target)
    return result

去重技巧

  • i > start and candidates[i] == candidates[i-1]: 同一层跳过重复元素
  • 需要先排序

5.3 排列问题

1. 全排列(LeetCode 46)

python 复制代码
def permute(nums):
    result = []
    n = len(nums)
    used = [False] * n
    
    def backtrack(path):
        if len(path) == n:
            result.append(path[:])
            return
        
        for i in range(n):
            if used[i]:
                continue
            
            used[i] = True
            path.append(nums[i])
            backtrack(path)
            path.pop()
            used[i] = False
    
    backtrack([])
    return result

2. 全排列II (LeetCode 47)

数组包含重复元素,但结果不能有重复排列。

python 复制代码
def permute_unique(nums):
    result = []
    nums.sort()
    n = len(nums)
    used = [False] * n
    
    def backtrack(path):
        if len(path) == n:
            result.append(path[:])
            return
        
        for i in range(n):
            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

去重原理

  • 排序后,相同元素相邻
  • not used[i-1]保证相同元素按顺序使用
  • 避免[1a, 2, 1b]和[1b, 2, 1a]都出现

5.4 子集问题

1. 子集(LeetCode 78)

python 复制代码
def subsets(nums):
    result = []
    
    def backtrack(start, path):
        result.append(path[:])  # 每个状态都是一个子集
        
        for i in range(start, len(nums)):
            path.append(nums[i])
            backtrack(i + 1, path)
            path.pop()
    
    backtrack(0, [])
    return result

2. 子集II (LeetCode 90)

数组包含重复元素。

python 复制代码
def subsets_with_dup(nums):
    result = []
    nums.sort()
    
    def backtrack(start, path):
        result.append(path[:])
        
        for i in range(start, len(nums)):
            if i > start and nums[i] == nums[i-1]:
                continue
            
            path.append(nums[i])
            backtrack(i + 1, path)
            path.pop()
    
    backtrack(0, [])
    return result

5.5 其他经典回溯问题

1. 括号生成 (LeetCode 22)

生成n对括号的所有合法组合。

python 复制代码
def generate_parenthesis(n):
    result = []
    
    def backtrack(path, left, right):
        if len(path) == 2 * n:
            result.append(''.join(path))
            return
        
        # 左括号:只要没用完就可以加
        if left < n:
            path.append('(')
            backtrack(path, left + 1, right)
            path.pop()
        
        # 右括号:只有当右括号少于左括号时才能加
        if right < left:
            path.append(')')
            backtrack(path, left, right + 1)
            path.pop()
    
    backtrack([], 0, 0)
    return result

关键点

  • 不是盲目枚举,而是根据括号配对规则剪枝
  • right < left保证括号合法性

2. N皇后(LeetCode 51,困难题)

python 复制代码
def solve_n_queens(n):
    result = []
    board = [['.'] * n for _ in range(n)]
    cols = set()
    diag1 = set()  # 左上到右下对角线
    diag2 = set()  # 右上到左下对角线
    
    def backtrack(row):
        if row == n:
            result.append([''.join(row) for row in board])
            return
        
        for col in range(n):
            # 检查是否可以放置皇后
            if col in cols or (row - col) in diag1 or (row + col) in diag2:
                continue
            
            # 放置皇后
            board[row][col] = 'Q'
            cols.add(col)
            diag1.add(row - col)
            diag2.add(row + col)
            
            backtrack(row + 1)
            
            # 回溯
            board[row][col] = '.'
            cols.remove(col)
            diag1.remove(row - col)
            diag2.remove(row + col)
    
    backtrack(0)
    return result

对角线判断技巧

  • 左上到右下: row - col相同
  • 右上到左下: row + col相同

3. 解数独(LeetCode 37,困难题)

python 复制代码
def solve_sudoku(board):
    rows = [set() for _ in range(9)]
    cols = [set() for _ in range(9)]
    boxes = [set() for _ in range(9)]
    empty = []
    
    # 初始化
    for i in range(9):
        for j in range(9):
            if board[i][j] == '.':
                empty.append((i, j))
            else:
                digit = board[i][j]
                rows[i].add(digit)
                cols[j].add(digit)
                boxes[(i // 3) * 3 + j // 3].add(digit)
    
    def backtrack(idx):
        if idx == len(empty):
            return True
        
        i, j = empty[idx]
        box_idx = (i // 3) * 3 + j // 3
        
        for digit in '123456789':
            if digit in rows[i] or digit in cols[j] or digit in boxes[box_idx]:
                continue
            
            # 放置数字
            board[i][j] = digit
            rows[i].add(digit)
            cols[j].add(digit)
            boxes[box_idx].add(digit)
            
            if backtrack(idx + 1):
                return True
            
            # 回溯
            board[i][j] = '.'
            rows[i].remove(digit)
            cols[j].remove(digit)
            boxes[box_idx].remove(digit)
        
        return False
    
    backtrack(0)

六、动态规划

6.1 动态规划核心思想

定义: 动态规划是一种将复杂问题分解为子问题,通过求解子问题并存储其结果,避免重复计算,从而高效求解原问题的算法思想。

与递归、分治的区别

  • 递归: 自顶向下,可能有大量重复计算
  • 分治: 子问题相互独立
  • 动态规划: 子问题重叠,通过记忆化避免重复计算

动态规划的三要素

  1. 最优子结构: 问题的最优解包含子问题的最优解
  2. 重叠子问题: 子问题会被多次求解
  3. 无后效性: 子问题的解一旦确定,就不会再改变

动态规划的解题步骤

  1. 定义状态: dp[i]或dp[i][j]表示什么
  2. 找状态转移方程: 如何从小问题推导到大问题
  3. 初始化: 确定边界条件
  4. 确定计算顺序: 保证计算dp[i]时,所需的子问题已经计算完成
  5. 优化空间复杂度(可选): 滚动数组等技巧

6.2 一维DP

6.2.1 爬楼梯类问题

1. 爬楼梯 (LeetCode 70)

每次可以爬1或2个台阶,爬到第n阶有多少种方法?

状态定义 : dp[i]表示爬到第i阶的方法数
转移方程 : dp[i] = dp[i-1] + dp[i-2]
初始条件: dp[0] = 1, dp[1] = 1

python 复制代码
def climb_stairs(n):
    if n <= 1:
        return 1
    
    dp = [0] * (n + 1)
    dp[0], dp[1] = 1, 1
    
    for i in range(2, n + 1):
        dp[i] = dp[i-1] + dp[i-2]
    
    return dp[n]

空间优化: 只需保存前两个状态

python 复制代码
def climb_stairs_optimized(n):
    if n <= 1:
        return 1
    
    prev2, prev1 = 1, 1
    
    for i in range(2, n + 1):
        curr = prev1 + prev2
        prev2 = prev1
        prev1 = curr
    
    return prev1

变体: 如果每次可以爬1、2或3个台阶呢?

python 复制代码
# 转移方程变为: dp[i] = dp[i-1] + dp[i-2] + dp[i-3]
# 需要保存前三个状态
6.2.2 打家劫舍系列

1. 打家劫舍 (LeetCode 198)

不能抢劫相邻的房子,求最大金额。

状态定义 : dp[i]表示抢劫前i个房子能获得的最大金额
转移方程 : dp[i] = max(dp[i-1], dp[i-2] + nums[i])
含义:

  • dp[i-1]: 不抢第i个房子
  • dp[i-2] + nums[i]: 抢第i个房子
python 复制代码
def rob(nums):
    if not nums:
        return 0
    if len(nums) == 1:
        return nums[0]
    
    n = len(nums)
    dp = [0] * n
    dp[0] = nums[0]
    dp[1] = max(nums[0], nums[1])
    
    for i in range(2, n):
        dp[i] = max(dp[i-1], dp[i-2] + nums[i])
    
    return dp[n-1]

空间优化:

python 复制代码
def rob_optimized(nums):
    prev2, prev1 = 0, 0
    
    for num in nums:
        curr = max(prev1, prev2 + num)
        prev2 = prev1
        prev1 = curr
    
    return prev1

2. 打家劫舍II (LeetCode 213)

房子围成一圈,首尾房子相邻,不能同时抢。

核心思路: 分两种情况

  • 抢第一个房子,不能抢最后一个: [0, n-2]
  • 不抢第一个房子,可以抢最后一个: [1, n-1]
python 复制代码
def rob_ii(nums):
    n = len(nums)
    if n == 1:
        return nums[0]
    if n == 2:
        return max(nums[0], nums[1])
    
    def rob_range(start, end):
        prev2, prev1 = 0, 0
        for i in range(start, end + 1):
            curr = max(prev1, prev2 + nums[i])
            prev2 = prev1
            prev1 = curr
        return prev1
    
    return max(rob_range(0, n-2), rob_range(1, n-1))

3. 打家劫舍III (LeetCode 337)

房子分布在二叉树上,相邻节点不能同时抢。

状态定义: 返回(不抢当前节点的最大值, 抢当前节点的最大值)

python 复制代码
def rob_iii(root):
    def dfs(node):
        if not node:
            return (0, 0)
        
        left = dfs(node.left)
        right = dfs(node.right)
        
        # 不抢当前节点:子节点可抢可不抢,取最大值
        not_rob = max(left) + max(right)
        
        # 抢当前节点:子节点一定不抢
        rob = node.val + left[0] + right[0]
        
        return (not_rob, rob)
    
    return max(dfs(root))
6.2.3 最大子数组和

最大子数组和(LeetCode 53)

状态定义 : dp[i]表示以nums[i]结尾的最大子数组和
转移方程 : dp[i] = max(nums[i], dp[i-1] + nums[i])
含义:

  • nums[i]: 从当前元素重新开始
  • dp[i-1] + nums[i]: 延续之前的子数组
python 复制代码
def max_sub_array(nums):
    if not nums:
        return 0
    
    dp = [0] * len(nums)
    dp[0] = nums[0]
    max_sum = dp[0]
    
    for i in range(1, len(nums)):
        dp[i] = max(nums[i], dp[i-1] + nums[i])
        max_sum = max(max_sum, dp[i])
    
    return max_sum

空间优化(Kadane算法):

python 复制代码
def max_sub_array_kadane(nums):
    max_sum = current_sum = nums[0]
    
    for num in nums[1:]:
        current_sum = max(num, current_sum + num)
        max_sum = max(max_sum, current_sum)
    
    return max_sum

变体 : 最大子数组乘积(LeetCode 152)

由于有负数,需要同时维护最大值和最小值。

python 复制代码
def max_product(nums):
    max_prod = min_prod = result = nums[0]
    
    for num in nums[1:]:
        if num < 0:
            max_prod, min_prod = min_prod, max_prod
        
        max_prod = max(num, max_prod * num)
        min_prod = min(num, min_prod * num)
        result = max(result, max_prod)
    
    return result

6.3 二维DP

6.3.1 路径问题

1. 不同路径 (LeetCode 62)

从左上角到右下角有多少条不同路径(只能向右或向下)。

状态定义 : dp[i][j]表示到达(i,j)的路径数
转移方程 : dp[i][j] = dp[i-1][j] + dp[i][j-1]
初始条件: 第一行和第一列都是1

python 复制代码
def unique_paths(m, n):
    dp = [[1] * n for _ in range(m)]
    
    for i in range(1, m):
        for j in range(1, n):
            dp[i][j] = dp[i-1][j] + dp[i][j-1]
    
    return dp[m-1][n-1]

空间优化: 只需要一行

python 复制代码
def unique_paths_optimized(m, n):
    dp = [1] * n
    
    for i in range(1, m):
        for j in range(1, n):
            dp[j] += dp[j-1]
    
    return dp[n-1]

2. 不同路径II (LeetCode 63)

网格中有障碍物。

python 复制代码
def unique_paths_with_obstacles(obstacle_grid):
    m, n = len(obstacle_grid), len(obstacle_grid[0])
    
    if obstacle_grid[0][0] == 1:
        return 0
    
    dp = [[0] * n for _ in range(m)]
    dp[0][0] = 1
    
    # 初始化第一列
    for i in range(1, m):
        dp[i][0] = 0 if obstacle_grid[i][0] == 1 else dp[i-1][0]
    
    # 初始化第一行
    for j in range(1, n):
        dp[0][j] = 0 if obstacle_grid[0][j] == 1 else dp[0][j-1]
    
    # 填表
    for i in range(1, m):
        for j in range(1, n):
            if obstacle_grid[i][j] == 1:
                dp[i][j] = 0
            else:
                dp[i][j] = dp[i-1][j] + dp[i][j-1]
    
    return dp[m-1][n-1]

3. 最小路径和 (LeetCode 64)

找出路径和最小的路径。

python 复制代码
def min_path_sum(grid):
    m, n = len(grid), len(grid[0])
    dp = [[0] * n for _ in range(m)]
    
    dp[0][0] = grid[0][0]
    
    # 初始化第一行
    for j in range(1, n):
        dp[0][j] = dp[0][j-1] + grid[0][j]
    
    # 初始化第一列
    for i in range(1, m):
        dp[i][0] = dp[i-1][0] + grid[i][0]
    
    # 填表
    for i in range(1, m):
        for j in range(1, n):
            dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j]
    
    return dp[m-1][n-1]
6.3.2 编辑距离

编辑距离 (LeetCode 72,困难题)

将word1转换为word2的最少操作数(插入、删除、替换)。

状态定义: dp[i][j]表示word1的前i个字符转换为word2的前j个字符的最少操作数

转移方程:

plain 复制代码
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],    # 删除word1[i-1]
        dp[i][j-1],    # 插入word2[j-1]
        dp[i-1][j-1]   # 替换word1[i-1]为word2[j-1]
    )
python 复制代码
def min_distance(word1, word2):
    m, n = len(word1), len(word2)
    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]
6.3.3 最长公共子序列

最长公共子序列(LeetCode 1143)

状态定义: dp[i][j]表示text1的前i个字符和text2的前j个字符的LCS长度

转移方程:

plain 复制代码
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])
python 复制代码
def longest_common_subsequence(text1, text2):
    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]

相关题目

  • 最长公共子串: 要求连续,转移方程不同
  • 最长递增子序列(LeetCode 300)

6.4 背包问题

背包问题是动态规划的经典应用,也是面试的高频考点。

6.4.1 0-1背包

问题描述: 有n个物品和容量为W的背包,每个物品有重量w[i]和价值v[i],每个物品只能选一次,求最大价值。

状态定义: dp[i][j]表示前i个物品,背包容量为j时的最大价值

转移方程:

plain 复制代码
dp[i][j] = max(
    dp[i-1][j],              # 不选第i个物品
    dp[i-1][j-w[i]] + v[i]   # 选第i个物品
)
python 复制代码
def knapsack_01(weights, values, capacity):
    n = len(weights)
    dp = [[0] * (capacity + 1) for _ in range(n + 1)]
    
    for i in range(1, n + 1):
        for j in range(1, capacity + 1):
            # 不选第i个物品
            dp[i][j] = dp[i-1][j]
            
            # 选第i个物品(如果放得下)
            if j >= weights[i-1]:
                dp[i][j] = max(dp[i][j], 
                              dp[i-1][j-weights[i-1]] + values[i-1])
    
    return dp[n][capacity]

空间优化: 一维数组,逆序遍历

python 复制代码
def knapsack_01_optimized(weights, values, capacity):
    dp = [0] * (capacity + 1)
    
    for i in range(len(weights)):
        # 逆序遍历,避免重复使用
        for j in range(capacity, weights[i] - 1, -1):
            dp[j] = max(dp[j], dp[j-weights[i]] + values[i])
    
    return dp[capacity]

相关题目

  • 分割等和子集(LeetCode 416): 判断能否分割为两个和相等的子集
  • 目标和(LeetCode 494): 给数组元素添加+/-使和为target
6.4.2 完全背包

问题描述: 每个物品可以选无限次。

转移方程:

plain 复制代码
dp[i][j] = max(
    dp[i-1][j],
    dp[i][j-w[i]] + v[i]  # 注意是dp[i]而不是dp[i-1]
)
python 复制代码
def knapsack_complete(weights, values, capacity):
    n = len(weights)
    dp = [[0] * (capacity + 1) for _ in range(n + 1)]
    
    for i in range(1, n + 1):
        for j in range(1, capacity + 1):
            dp[i][j] = dp[i-1][j]
            if j >= weights[i-1]:
                dp[i][j] = max(dp[i][j],
                              dp[i][j-weights[i-1]] + values[i-1])
    
    return dp[n][capacity]

空间优化: 正序遍历(允许重复使用)

python 复制代码
def knapsack_complete_optimized(weights, values, capacity):
    dp = [0] * (capacity + 1)
    
    for i in range(len(weights)):
        # 正序遍历,允许重复使用
        for j in range(weights[i], capacity + 1):
            dp[j] = max(dp[j], dp[j-weights[i]] + values[i])
    
    return dp[capacity]

相关题目

  • 零钱兑换(LeetCode 322): 最少硬币数
  • 零钱兑换II(LeetCode 518): 组合数

七、贪心算法

7.1 贪心算法核心思想

定义: 贪心算法在每一步选择中都采取当前状态下的最优选择,期望通过局部最优达到全局最优。

与动态规划的区别

  • 贪心: 只看当前最优,不考虑后续影响
  • 动态规划: 考虑所有可能,保存子问题的解

适用条件

  1. 贪心选择性质: 局部最优能导致全局最优
  2. 最优子结构: 问题的最优解包含子问题的最优解
  3. 无后效性: 当前选择不影响之前的选择

注意: 贪心算法不一定能得到全局最优解,需要通过数学证明或反例验证。

7.2 区间问题

区间问题是贪心算法的典型应用场景。

7.2.1 区间调度

无重叠区间 (LeetCode 435)

给定一组区间,找出需要移除的最少区间数,使剩余区间不重叠。

贪心策略: 按结束时间排序,优先选择结束早的区间

python 复制代码
def erase_overlap_intervals(intervals):
    if not intervals:
        return 0
    
    # 按结束时间排序
    intervals.sort(key=lambda x: x[1])
    
    count = 1  # 第一个区间一定选
    end = intervals[0][1]
    
    for i in range(1, len(intervals)):
        # 如果不重叠,选择该区间
        if intervals[i][0] >= end:
            count += 1
            end = intervals[i][1]
    
    # 需要移除的区间数
    return len(intervals) - count

正确性证明: 选择结束最早的区间,为后续区间留出更多空间。

相关题目

  • 用最少数量的箭引爆气球(LeetCode 452)
  • 会议室II(LeetCode 253): 需要的最少会议室数
7.2.2 区间合并

合并区间(LeetCode 56)

python 复制代码
def merge(intervals):
    if not intervals:
        return []
    
    # 按起始时间排序
    intervals.sort(key=lambda x: x[0])
    merged = [intervals[0]]
    
    for i in range(1, len(intervals)):
        # 如果当前区间与上一个区间重叠
        if intervals[i][0] <= merged[-1][1]:
            # 合并区间
            merged[-1][1] = max(merged[-1][1], intervals[i][1])
        else:
            # 不重叠,添加新区间
            merged.append(intervals[i])
    
    return merged

7.3 贪心+排序

很多贪心问题需要先排序,然后根据某种策略选择。

分发饼干(LeetCode 455)

python 复制代码
def find_content_children(g, s):
    g.sort()  # 孩子的胃口
    s.sort()  # 饼干尺寸
    
    child = cookie = 0
    while child < len(g) and cookie < len(s):
        if s[cookie] >= g[child]:
            child += 1
        cookie += 1
    
    return child

贪心策略: 用最小的饼干满足最小胃口的孩子

7.4 其他贪心问题

跳跃游戏 (LeetCode 55)

判断能否跳到最后一个位置。

python 复制代码
def can_jump(nums):
    max_reach = 0
    
    for i in range(len(nums)):
        # 如果当前位置不可达
        if i > max_reach:
            return False
        
        # 更新最远可达位置
        max_reach = max(max_reach, i + nums[i])
        
        # 如果已经可以到达最后
        if max_reach >= len(nums) - 1:
            return True
    
    return True

跳跃游戏II (LeetCode 45,中等偏难)

求到达最后位置的最少跳跃次数。

python 复制代码
def jump(nums):
    if len(nums) <= 1:
        return 0
    
    jumps = 0
    current_end = 0
    farthest = 0
    
    for i in range(len(nums) - 1):
        farthest = max(farthest, i + nums[i])
        
        # 到达当前跳跃的边界
        if i == current_end:
            jumps += 1
            current_end = farthest
            
            # 如果已经可以到达最后
            if current_end >= len(nums) - 1:
                break
    
    return jumps

八、其他重要算法

8.1 二分查找

核心思想: 在有序数组中,每次比较中间元素,将搜索范围缩小一半,时间复杂度O(log n)。

标准模板

python 复制代码
def binary_search(arr, target):
    left, right = 0, len(arr) - 1
    
    while left <= right:
        mid = left + (right - left) // 2  # 防止溢出
        
        if arr[mid] == target:
            return mid
        elif arr[mid] < target:
            left = mid + 1
        else:
            right = mid - 1
    
    return -1

变体

1. 寻找左边界

python 复制代码
def left_bound(arr, target):
    left, right = 0, len(arr)
    
    while left < right:
        mid = left + (right - left) // 2
        if arr[mid] < target:
            left = mid + 1
        else:
            right = mid
    
    return left

2. 寻找右边界

python 复制代码
def right_bound(arr, target):
    left, right = 0, len(arr)
    
    while left < right:
        mid = left + (right - left) // 2
        if arr[mid <= target:
            left = mid + 1
        else:
            right = mid
    
    return left - 1

经典题目

  • 搜索旋转排序数组(LeetCode 33)
  • 寻找旋转排序数组中的最小值(LeetCode 153)
  • 在排序数组中查找元素的第一个和最后一个位置(LeetCode 34)

8.2 排序算法

虽然实际编程中常用库函数,但理解排序算法原理对算法思维训练很有帮助。

常见排序算法性能对比

排序算法 平均时间 最坏时间 空间复杂度 稳定性
冒泡排序 O(n²) O(n²) O(1) 稳定
选择排序 O(n²) O(n²) O(1) 不稳定
插入排序 O(n²) O(n²) O(1) 稳定
快速排序 O(n log n) O(n²) O(log n) 不稳定
归并排序 O(n log n) O(n log n) O(n) 稳定
堆排序 O(n log n) O(n log n) O(1) 不稳定

快速排序实现

python 复制代码
def quick_sort(arr):
    if len(arr) <= 1:
        return arr
    
    pivot = arr[len(arr) // 2]
    left = [x for x in arr if x < pivot]
    middle = [x for x in arr if x == pivot]
    right = [x for x in arr if x > pivot]
    
    return quick_sort(left) + middle + quick_sort(right)

归并排序实现

python 复制代码
def merge_sort(arr):
    if len(arr) <= 1:
        return arr
    
    mid = len(arr) // 2
    left = merge_sort(arr[:mid])
    right = merge_sort(arr[mid:])
    
    return merge(left, right)

def merge(left, right):
    result = []
    i = j = 0
    
    while i < len(left) and j < len(right):
        if left[i] <= right[j]:
            result.append(left[i])
            i += 1
        else:
            result.append(right[j])
            j += 1
    
    result.extend(left[i:])
    result.extend(right[j:])
    return result

8.3 位运算

位运算在某些场景下可以大幅提升性能。

常用技巧

操作 实现 说明
判断奇偶 n & 1 奇数为1,偶数为0
除以2 n >> 1 右移一位
乘以2 n << 1 左移一位
取反 ~n 按位取反
消除最后的1 n & (n-1) 常用于计数1的个数
获取最后的1 n & (-n) 提取最低位的1

经典题目

1. 只出现一次的数字(LeetCode 136)

python 复制代码
def single_number(nums):
    result = 0
    for num in nums:
        result ^= num  # 异或
    return result

原理: a ^ a = 0, a ^ 0 = a

2. 位1的个数(LeetCode 191)

python 复制代码
def hamming_weight(n):
    count = 0
    while n:
        n &= n - 1  # 消除最后一个1
        count += 1
    return count

3. 颠倒二进制位(LeetCode 190)

python 复制代码
def reverse_bits(n):
    result = 0
    for _ in range(32):
        result = (result << 1) | (n & 1)
        n >>= 1
    return result

九、算法学习路线与技巧总结

9.1 刷题顺序建议

基于LeetCode Hot 100和牛客Top 101的学习路线:

第一阶段:基础数据结构(2-3周)

  1. 数组与字符串(双指针、滑动窗口)
  2. 链表(虚拟头节点、快慢指针、反转)
  3. 栈和队列(单调栈、单调队列)
  4. 哈希表(两数之和系列)

第二阶段:树与图(2-3周)

  1. 二叉树遍历(递归与迭代)
  2. 二叉树路径问题
  3. 二叉搜索树
  4. DFS/BFS
  5. 岛屿问题

第三阶段:核心算法(3-4周)

  1. 回溯(组合、排列、子集、N皇后)
  2. 动态规划(一维DP → 二维DP → 背包问题)
  3. 贪心(区间问题、跳跃游戏)
  4. 二分查找

第四阶段:进阶与复习(2周)

  1. 困难题攻克
  2. 专题总结
  3. 模拟面试

9.2 解题技巧总结

通用思路

  1. 理解题意: 仔细读题,明确输入输出,考虑边界条件
  2. 暴力求解: 先想出暴力解法,分析时间复杂度
  3. 寻找优化 :
    • 能否用哈希表空间换时间?
    • 能否先排序?
    • 能否用双指针优化?
    • 是否有单调性可以二分?
  4. 分类讨论: 复杂问题分情况讨论
  5. 画图辅助: 画出数据结构的图形表示
  6. 写伪代码: 先用伪代码理清思路
  7. 编码实现: 注意边界条件和特殊情况
  8. 测试用例 :
    • 正常用例
    • 边界用例(空、单元素、极值)
    • 特殊用例

Debug技巧

  • 添加打印语句查看中间状态
  • 使用调试器单步执行
  • 用小数据手工模拟
  • 检查数组越界
  • 检查整数溢出
  • 检查空指针

9.3 时间管理

刷题时间分配

  • 简单题: 15-30分钟
  • 中等题: 30-60分钟
  • 困难题: 60-120分钟

如果卡住

  • 15分钟无思路: 看提示
  • 30分钟无进展: 看题解
  • 看完题解:
    1. 理解思路
    2. 自己实现
    3. 第二天再做一遍

9.4 常见错误

思维错误

  1. 没有考虑边界条件
  2. 忘记回溯(修改后没有恢复)
  3. 混淆索引和长度
  4. 死循环(循环条件错误)

代码错误

  1. 数组越界
  2. 整数溢出
  3. 浮点数比较
  4. 深拷贝vs浅拷贝
  5. 引用传递问题

9.5 刷题心态

正确心态

  • 刷题是练习思维,不是背答案
  • 遇到困难很正常,不要气馁
  • 重复刷题是必要的,第一遍不会很正常
  • 重视理解原理,而非死记代码

刷题节奏

  • 每天1-3题
  • 周末复习一周的题目
  • 每月总结专题
  • 面试前集中复习

十、总结

算法学习是一个循序渐进的过程,需要理解原理、掌握模板、大量练习。

本文系统地梳理了LeetCode Hot 100和牛客Top 101中的核心题型:

数据结构类

  • 数组: 双指针、滑动窗口、前缀和
  • 链表: 快慢指针、反转、合并
  • 栈队列: 单调栈、单调队列
  • 哈希表: 快速查找、计数统计
  • 树: 遍历、路径、BST、序列化

算法思想类

  • 搜索: DFS(回溯)、BFS、拓扑排序
  • 动态规划: 一维DP、二维DP、背包问题
  • 贪心: 区间问题、跳跃游戏
  • 分治: 归并排序、快速排序
  • 二分: 标准二分、变体

核心建议

  1. 掌握基本数据结构的操作
  2. 理解算法思想的本质
  3. 熟记常用模板
  4. 大量练习形成肌肉记忆
  5. 定期复习巩固

参考文献

  1. LeetCode官方网站 - 提供了海量算法题目和社区讨论
    https://leetcode.cn/
  2. 牛客网算法题库 - 国内优秀的算法练习平台
    https://www.nowcoder.com/
  3. 《算法导论》(Introduction to Algorithms) - 算法领域的经典教材,全面系统地介绍了算法设计与分析
    作者: Thomas H. Cormen等
  4. 《数据结构与算法之美》专栏 - 极客时间 - 通俗易懂的算法学习专栏
    https://time.geekbang.org/column/intro/126
  5. LeetCode Hot 100题解汇总 - GitHub - 开源的题解集合
    https://github.com/tonngw/LeetCode021
  6. OI Wiki - 算法竞赛知识整合站点 - 提供算法竞赛相关的知识和技巧
    https://oi-wiki.org/
  7. Big O Cheat Sheet - 常用数据结构和算法的复杂度速查表
    http://bigocheatsheet.com/
  8. 《算法》第4版 (Algorithms, 4th Edition) - 由Robert Sedgewick编写的经典算法教材
  9. LeetCode中国区题解精选 - CSDN博客 - 包含大量优质题解和学习心得
    https://blog.csdn.net/
  10. 代码随想录 - 系统的算法学习路线和详细题解
    https://programmercarl.com/
  11. 算法模板 - 知乎专栏 - 总结了各类算法的通用模板
    https://www.zhihu.com/
  12. 《剑指Offer》 - 面试算法题的经典参考书
    作者: 何海涛
相关推荐
Kent_J_Truman3 小时前
LeetCode Hot100 自用
算法·leetcode·职场和发展
Victory_orsh4 小时前
“自然搞懂”深度学习(基于Pytorch架构)——010203
人工智能·pytorch·python·深度学习·神经网络·算法·机器学习
CoovallyAIHub4 小时前
突破360°跟踪极限!OmniTrack++:全景MOT新范式,HOTA指标狂飙43%
深度学习·算法·计算机视觉
得物技术4 小时前
得物管理类目配置线上化:从业务痛点到技术实现
后端·算法·数据分析
CoovallyAIHub5 小时前
首个大规模、跨模态医学影像编辑数据集,Med-Banana-50K数据集专为医学AI打造(附数据集地址)
深度学习·算法·计算机视觉
熬了夜的程序员5 小时前
【LeetCode】101. 对称二叉树
算法·leetcode·链表·职场和发展·矩阵
却道天凉_好个秋5 小时前
目标检测算法与原理(二):Tensorflow实现迁移学习
算法·目标检测·tensorflow
柳鲲鹏6 小时前
RGB转换为NV12,查表式算法
linux·c语言·算法