当前位置: 首页 > news >正文

算法题种类与解题思路全面指南:基于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题型统计

题型题目数量占比难度分布
链表1514.9%简单-中等
二叉树1716.8%简单-困难
动态规划1615.8%中等-困难
递归/回溯109.9%中等-困难
排序98.9%简单-中等
双指针87.9%简单-中等
二分查找76.9%中等
哈希65.9%简单-中等
堆/栈/队列76.9%中等
其他65.9%不定

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


二、基础数据结构类题目

2.1 数组与矩阵

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

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

常见题型与解题思路

2.1.1 双指针技巧

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

对撞指针(两端向中间)

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

快慢指针(同向移动)

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

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

原理

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

经典题目

  • 和为K的子数组(LeetCode 560): 前缀和 + 哈希表
  • 矩阵区域和检索(LeetCode 304): 二维前缀和
# 一维前缀和模板
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 滑动窗口

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

适用条件

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

核心框架

def sliding_window(s):left = 0window = {}  # 窗口内的数据result = 0for right in range(len(s)):# 扩大窗口c = s[right]window[c] = window.get(c, 0) + 1# 收缩窗口while 窗口需要收缩:d = s[left]window[d] -= 1left += 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)
# 螺旋遍历模板
def spiral_order(matrix):if not matrix: return []result = []top, bottom = 0, len(matrix) - 1left, right = 0, len(matrix[0]) - 1while 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 -= 1if top <= bottom:# 从右到左for j in range(right, left - 1, -1):result.append(matrix[bottom][j])bottom -= 1if left <= right:# 从下到上for i in range(bottom, top - 1, -1):result.append(matrix[i][left])left += 1return result

2.2 链表

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

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

链表题目的通用技巧

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

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

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

应用场景

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

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

典型应用

  1. 找中点(快指针走2步,慢指针走1步)
def find_middle(head):slow = fast = headwhile fast and fast.next:slow = slow.nextfast = fast.next.nextreturn slow  # slow指向中点
  1. 检测环(Floyd判圈算法)
def has_cycle(head):slow = fast = headwhile fast and fast.next:slow = slow.nextfast = fast.next.nextif slow == fast:return Truereturn False
  1. 找环的入口
def detect_cycle(head):slow = fast = head# 第一阶段:判断是否有环while fast and fast.next:slow = slow.nextfast = fast.next.nextif slow == fast:breakelse:return None# 第二阶段:找入口slow = headwhile slow != fast:slow = slow.nextfast = fast.nextreturn 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个节点
def remove_nth_from_end(head, n):dummy = ListNode(0)dummy.next = headfast = slow = dummy# fast先走n+1步for _ in range(n + 1):fast = fast.next# fast和slow一起走,直到fast到达末尾while fast:fast = fast.nextslow = slow.next# 删除slow.nextslow.next = slow.next.nextreturn dummy.next
2.2.3 链表反转

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

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

def reverse_list(head):prev = Nonecurr = headwhile curr:next_temp = curr.next  # 保存下一个节点curr.next = prev       # 反转指针prev = curr            # prev前进curr = next_temp       # curr前进return prev

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

def reverse_list_recursive(head):if not head or not head.next:return headnew_head = reverse_list_recursive(head.next)head.next.next = head  # 关键步骤head.next = Nonereturn new_head

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

def reverse_between(head, left, right):dummy = ListNode(0)dummy.next = headpre = dummy# 找到left前一个节点for _ in range(left - 1):pre = pre.next# 反转left到right部分curr = pre.nextfor _ in range(right - left):next_node = curr.nextcurr.next = next_node.nextnext_node.next = pre.nextpre.next = next_nodereturn dummy.next

4. K个一组翻转链表(LeetCode 25,困难题)
这是链表反转的最难变体,需要结合多种技巧:

  • 计算链表长度,判断是否需要反转
  • 分组反转
  • 连接各个反转后的组
def reverse_k_group(head, k):dummy = ListNode(0)dummy.next = headpre = dummywhile True:# 检查剩余节点是否够k个tail = prefor _ in range(k):tail = tail.nextif not tail:return dummy.next# 记录下一组的前驱节点next_group = tail.next# 反转当前组head, tail = reverse_one_group(pre.next, tail)# 连接pre.next = headtail.next = next_grouppre = taildef reverse_one_group(head, tail):prev = tail.nextcurr = headwhile prev != tail:next_temp = curr.nextcurr.next = prevprev = currcurr = next_tempreturn tail, head
2.2.4 合并链表

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

def merge_two_lists(l1, l2):dummy = ListNode(0)curr = dummywhile l1 and l2:if l1.val < l2.val:curr.next = l1l1 = l1.nextelse:curr.next = l2l2 = l2.nextcurr = curr.nextcurr.next = l1 if l1 else l2return dummy.next

2. 合并K个有序链表(LeetCode 23,困难题)
使用最小堆(优先队列)维护K个链表的当前最小节点。

import heapqdef merge_k_lists(lists):dummy = ListNode(0)curr = dummyheap = []# 初始化堆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 = nodecurr = curr.nextif 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)
def sort_list(head):if not head or not head.next:return head# 快慢指针找中点slow, fast = head, head.nextwhile fast and fast.next:slow = slow.nextfast = fast.next.next# 分割链表mid = slow.nextslow.next = None# 递归排序left = sort_list(head)right = sort_list(mid)# 合并return merge(left, right)def merge(l1, l2):dummy = ListNode(0)curr = dummywhile l1 and l2:if l1.val < l2.val:curr.next = l1l1 = l1.nextelse:curr.next = l2l2 = l2.nextcurr = curr.nextcurr.next = l1 if l1 else l2return dummy.next

2.3 栈与队列

核心特点

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

1. 括号匹配(LeetCode 20)
这是栈最直接的应用,利用栈的LIFO特性匹配成对括号。

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

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

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)
栈天然适合处理后缀表达式。

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)

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

工作原理

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

2. 接雨水(LeetCode 42,困难题)
这是单调栈的经典应用,也可以用双指针解决。

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

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

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

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

def largest_rectangle_area(heights):stack = []max_area = 0heights = [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] - 1max_area = max(max_area, h * w)stack.append(i)return max_area

原理

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

1. 用栈实现队列(LeetCode 232)
用两个栈实现队列的所有操作。

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,困难题)
使用单调队列(双端队列)维护窗口内的最大值。

from collections import dequedef 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的第一题,也是哈希表应用的经典例子。

def two_sum(nums, target):hash_map = {}for i, num in enumerate(nums):complement = target - numif complement in hash_map:return [hash_map[complement], i]hash_map[num] = ireturn []

优化思路

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

2. 字母异位词分组(LeetCode 49)
核心是找到一个标准化的key来表示异位词。

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

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

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

3. 最长连续序列(LeetCode 128)
要求O(n)时间复杂度,因此不能排序。

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

关键优化

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

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

class Node:def __init__(self, key=0, value=0):self.key = keyself.value = valueself.prev = Noneself.next = Noneclass LRUCache:def __init__(self, capacity):self.cache = {}  # key -> Nodeself.capacity = capacity# 虚拟头尾节点self.head = Node()self.tail = Node()self.head.next = self.tailself.tail.prev = self.headdef get(self, key):if key not in self.cache:return -1node = self.cache[key]self._move_to_head(node)return node.valuedef put(self, key, value):if key in self.cache:node = self.cache[key]node.value = valueself._move_to_head(node)else:node = Node(key, value)self.cache[key] = nodeself._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.headnode.next = self.head.nextself.head.next.prev = nodeself.head.next = nodedef _remove_node(self, node):node.prev.next = node.nextnode.next.prev = node.prevdef _move_to_head(self, node):self._remove_node(node)self._add_to_head(node)def _remove_tail(self):node = self.tail.prevself._remove_node(node)return node

数据结构选择

  • 哈希表: O(1)查找
  • 双向链表: O(1)插入和删除
  • 两者结合: 实现所有操作O(1)

三、树相关算法

3.1 二叉树基础

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

二叉树节点定义

class TreeNode:def __init__(self, val=0, left=None, right=None):self.val = valself.left = leftself.right = right
3.1.1 二叉树遍历

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

递归实现:

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

迭代实现(用栈):

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

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

递归实现:

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

迭代实现:

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

重要性质

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

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

递归实现:

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

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

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)

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

from collections import dequedef 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)

递归解法:

def max_depth(root):if not root:return 0return 1 + max(max_depth(root.left), max_depth(root.right))

迭代解法(BFS):

def max_depth_iterative(root):if not root:return 0depth = 0queue = deque([root])while queue:depth += 1for _ 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)
注意最小深度的定义:从根节点到最近叶子节点的路径长度。

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)。

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

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

3.1.3 二叉树路径问题

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

def has_path_sum(root, target_sum):if not root:return Falseif not root.left and not root.right:return root.val == target_sumreturn (has_path_sum(root.left, target_sum - root.val) orhas_path_sum(root.right, target_sum - root.val))

2. 路径总和II(LeetCode 113)
找出所有满足条件的路径。

def path_sum(root, target_sum):result = []def dfs(node, path, remain):if not node:returnpath.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,中等偏难)
路径不一定从根开始,也不一定到叶子结束。

def path_sum_iii(root, target_sum):def count_paths(node, current_sum):if not node:return 0current_sum += node.valcount = 0# 以当前节点结束的路径数if current_sum == target_sum:count += 1# 继续向下搜索count += count_paths(node.left, current_sum)count += count_paths(node.right, current_sum)return countif 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)。

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

4. 二叉树的最大路径和(LeetCode 124,困难题)
路径可以从任意节点开始到任意节点结束。

def max_path_sum(root):max_sum = float('-inf')def max_gain(node):nonlocal max_sumif 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_gainmax_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)

错误的做法:

# 错误! 只比较了父子节点
def is_valid_bst_wrong(root):if not root:return Trueif root.left and root.left.val >= root.val:return Falseif root.right and root.right.val <= root.val:return Falsereturn is_valid_bst_wrong(root.left) and is_valid_bst_wrong(root.right)

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

def is_valid_bst(root):def validate(node, min_val, max_val):if not node:return Trueif node.val <= min_val or node.val >= max_val:return Falsereturn (validate(node.left, min_val, node.val) andvalidate(node.right, node.val, max_val))return validate(root, float('-inf'), float('inf'))

利用中序遍历:

def is_valid_bst_inorder(root):prev = float('-inf')stack = []curr = rootwhile curr or stack:while curr:stack.append(curr)curr = curr.leftcurr = stack.pop()if curr.val <= prev:return Falseprev = curr.valcurr = curr.rightreturn True

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

def search_bst(root, val):if not root or root.val == val:return rootif val < root.val:return search_bst(root.left, val)else:return search_bst(root.right, val)

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

def search_bst_iterative(root, val):while root:if val == root.val:return rootelif val < root.val:root = root.leftelse:root = root.rightreturn None

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

利用BST中序遍历的性质:

def kth_smallest(root, k):stack = []curr = rootcount = 0while curr or stack:while curr:stack.append(curr)curr = curr.leftcurr = stack.pop()count += 1if count == k:return curr.valcurr = curr.rightreturn -1
3.2.2 BST的修改操作

1. 插入节点(LeetCode 701)

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. 删除有两个子节点的节点: 用后继节点(右子树最小值)或前驱节点(左子树最大值)替代
def delete_node(root, key):if not root:return Noneif 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.rightif not root.right:return root.left# 有两个子节点:用后继节点替代successor = find_min(root.right)root.val = successor.valroot.right = delete_node(root.right, successor.val)return rootdef find_min(node):while node.left:node = node.leftreturn node
3.2.3 BST的构造

1. 有序数组转BST(LeetCode 108)
要构造高度平衡的BST,选择中间元素作为根。

def sorted_array_to_bst(nums):if not nums:return Nonemid = len(nums) // 2root = 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,中等偏难)

def build_tree(preorder, inorder):if not preorder:return Noneroot_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

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

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 Noneroot_val = preorder[pre_start]root = TreeNode(root_val)root_idx = inorder_map[root_val]left_size = root_idx - in_startroot.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 rootreturn helper(0, len(preorder) - 1, 0, len(inorder) - 1)

3.3 二叉树的高级操作

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

序列化(LeetCode 297,困难题)
将树结构转换为字符串,并能从字符串恢复树结构。

前序遍历方式:

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 Nonenode = TreeNode(int(val))node.left = helper(values)node.right = helper(values)return nodevalues = iter(data.split(','))return helper(values)

层序遍历方式:

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 Nonevalues = data.split(',')root = TreeNode(int(values[0]))queue = deque([root])i = 1while queue:node = queue.popleft()if values[i] != "null":node.left = TreeNode(int(values[i]))queue.append(node.left)i += 1if values[i] != "null":node.right = TreeNode(int(values[i]))queue.append(node.right)i += 1return root
3.3.2 最近公共祖先

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

def lowest_common_ancestor(root, p, q):if not root or root == p or root == q:return rootleft = 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的性质可以简化算法。

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分别在两侧,或其中一个就是rootelse:return rootreturn None

四、搜索与图论

4.1 深度优先搜索(DFS)

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

实现方式

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

经典应用场景

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

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

1. 岛屿数量(LeetCode 200)

def num_islands(grid):if not grid:return 0m, n = len(grid), len(grid[0])count = 0def 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'# 向四个方向DFSdfs(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 += 1dfs(i, j)return count

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

def max_area_of_island(grid):m, n = len(grid), len(grid[0])max_area = 0def dfs(i, j):if i < 0 or i >= m or j < 0 or j >= n or grid[i][j] != 1:return 0grid[i][j] = 0area = 1area += dfs(i + 1, j)area += dfs(i - 1, j)area += dfs(i, j + 1)area += dfs(i, j - 1)return areafor 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’。

def solve(board):if not board:returnm, 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':returnboard[i][j] = 'T'  # 临时标记dfs(i + 1, j)dfs(i - 1, j)dfs(i, j + 1)dfs(i, j - 1)# 从边界DFSfor 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)
在二维网格中搜索单词,可以上下左右移动,但不能重复使用同一个格子。

def exist(board, word):m, n = len(board), len(board[0])def dfs(i, j, k):# k是word中当前要匹配的字符索引if k == len(word):return Trueif 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) ordfs(i - 1, j, k + 1) ordfs(i, j + 1, k + 1) ordfs(i, j - 1, k + 1))# 回溯board[i][j] = tempreturn foundfor i in range(m):for j in range(n):if dfs(i, j, 0):return Truereturn False

关键点

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

4.2 广度优先搜索(BFS)

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

实现方式
使用队列

适用场景

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

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

from collections import dequedef shortest_path_binary_matrix(grid):n = len(grid)if grid[0][0] == 1 or grid[n-1][n-1] == 1:return -1if n == 1:return 1queue = 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 + dcif r == n-1 and c == n-1:return dist + 1if 0 <= r < n and 0 <= c < n and grid[r][c] == 0:grid[r][c] = 1queue.append((r, c, dist + 1))return -1

2. 单词接龙(LeetCode 127,困难题)
从beginWord变换到endWord,每次只能改变一个字母,且中间词必须在字典中。

def ladder_length(begin_word, end_word, word_list):if end_word not in word_list:return 0word_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,可以大幅减少搜索空间。

def ladder_length_bidirectional(begin_word, end_word, word_list):if end_word not in word_list:return 0word_set = set(word_list)begin_set = {begin_word}end_set = {end_word}length = 1while begin_set and end_set:# 总是从较小的集合开始扩展if len(begin_set) > len(end_set):begin_set, end_set = end_set, begin_setnext_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 + 1if next_word in word_set:word_set.remove(next_word)next_set.add(next_word)begin_set = next_setlength += 1return 0

4.3 拓扑排序

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

应用场景

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

算法思路(Kahn算法)

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

课程表(LeetCode 207)

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

课程表II(LeetCode 210)
在判断能否完成的基础上,返回一个可行的课程顺序。

def find_order(num_courses, prerequisites):graph = defaultdict(list)in_degree = [0] * num_coursesfor course, prereq in prerequisites:graph[prereq].append(course)in_degree[course] += 1queue = 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] -= 1if in_degree[next_course] == 0:queue.append(next_course)return result if len(result) == num_courses else []

五、回溯算法

5.1 回溯算法核心思想

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

本质: 暴力搜索 + 剪枝

适用场景

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

回溯算法模板

def backtrack(路径, 选择列表):if 满足结束条件:result.add(路径)returnfor 选择 in 选择列表:做选择backtrack(路径, 选择列表)撤销选择  # 回溯

5.2 组合问题

1. 组合(LeetCode 77)
从1到n中选k个数的所有组合。

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

关键点

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

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

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

3. 组合总和II(LeetCode 40)
数组有重复元素,每个元素只能使用一次,但结果不能有重复组合。

def combination_sum2(candidates, target):result = []candidates.sort()def backtrack(start, path, remain):if remain == 0:result.append(path[:])returnfor i in range(start, len(candidates)):if candidates[i] > remain:break# 去重:同一层不能使用相同的元素if i > start and candidates[i] == candidates[i-1]:continuepath.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)

def permute(nums):result = []n = len(nums)used = [False] * ndef backtrack(path):if len(path) == n:result.append(path[:])returnfor i in range(n):if used[i]:continueused[i] = Truepath.append(nums[i])backtrack(path)path.pop()used[i] = Falsebacktrack([])return result

2. 全排列II(LeetCode 47)
数组包含重复元素,但结果不能有重复排列。

def permute_unique(nums):result = []nums.sort()n = len(nums)used = [False] * ndef backtrack(path):if len(path) == n:result.append(path[:])returnfor i in range(n):if used[i]:continue# 去重:如果当前元素与前一个元素相同,且前一个元素未被使用,则跳过if i > 0 and nums[i] == nums[i-1] and not used[i-1]:continueused[i] = Truepath.append(nums[i])backtrack(path)path.pop()used[i] = Falsebacktrack([])return result

去重原理

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

5.4 子集问题

1. 子集(LeetCode 78)

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)
数组包含重复元素。

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]:continuepath.append(nums[i])backtrack(i + 1, path)path.pop()backtrack(0, [])return result

5.5 其他经典回溯问题

1. 括号生成(LeetCode 22)
生成n对括号的所有合法组合。

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,困难题)

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])returnfor 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,困难题)

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 Truei, j = empty[idx]box_idx = (i // 3) * 3 + j // 3for digit in '123456789':if digit in rows[i] or digit in cols[j] or digit in boxes[box_idx]:continue# 放置数字board[i][j] = digitrows[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 Falsebacktrack(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

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

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

def climb_stairs_optimized(n):if n <= 1:return 1prev2, prev1 = 1, 1for i in range(2, n + 1):curr = prev1 + prev2prev2 = prev1prev1 = currreturn prev1

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

# 转移方程变为: 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个房子
def rob(nums):if not nums:return 0if len(nums) == 1:return nums[0]n = len(nums)dp = [0] * ndp[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]

空间优化:

def rob_optimized(nums):prev2, prev1 = 0, 0for num in nums:curr = max(prev1, prev2 + num)prev2 = prev1prev1 = currreturn prev1

2. 打家劫舍II(LeetCode 213)
房子围成一圈,首尾房子相邻,不能同时抢。

核心思路: 分两种情况

  • 抢第一个房子,不能抢最后一个: [0, n-2]
  • 不抢第一个房子,可以抢最后一个: [1, n-1]
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, 0for i in range(start, end + 1):curr = max(prev1, prev2 + nums[i])prev2 = prev1prev1 = currreturn prev1return max(rob_range(0, n-2), rob_range(1, n-1))

3. 打家劫舍III(LeetCode 337)
房子分布在二叉树上,相邻节点不能同时抢。

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

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]: 延续之前的子数组
def max_sub_array(nums):if not nums:return 0dp = [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算法):

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)
由于有负数,需要同时维护最大值和最小值。

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_prodmax_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

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]

空间优化: 只需要一行

def unique_paths_optimized(m, n):dp = [1] * nfor i in range(1, m):for j in range(1, n):dp[j] += dp[j-1]return dp[n-1]

2. 不同路径II(LeetCode 63)
网格中有障碍物。

def unique_paths_with_obstacles(obstacle_grid):m, n = len(obstacle_grid), len(obstacle_grid[0])if obstacle_grid[0][0] == 1:return 0dp = [[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] = 0else:dp[i][j] = dp[i-1][j] + dp[i][j-1]return dp[m-1][n-1]

3. 最小路径和(LeetCode 64)
找出路径和最小的路径。

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个字符的最少操作数

转移方程:

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])
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] = ifor 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长度

转移方程:

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])
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] + 1else: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时的最大价值

转移方程:

dp[i][j] = max(dp[i-1][j],              # 不选第i个物品dp[i-1][j-w[i]] + v[i]   # 选第i个物品
)
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]

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

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 完全背包

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

转移方程:

dp[i][j] = max(dp[i-1][j],dp[i][j-w[i]] + v[i]  # 注意是dp[i]而不是dp[i-1]
)
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]

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

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)
给定一组区间,找出需要移除的最少区间数,使剩余区间不重叠。

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

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 += 1end = intervals[i][1]# 需要移除的区间数return len(intervals) - count

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

相关题目

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

合并区间(LeetCode 56)

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)

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

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

7.4 其他贪心问题

跳跃游戏(LeetCode 55)
判断能否跳到最后一个位置。

def can_jump(nums):max_reach = 0for 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 Truereturn True

跳跃游戏II(LeetCode 45,中等偏难)
求到达最后位置的最少跳跃次数。

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

八、其他重要算法

8.1 二分查找

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

标准模板

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

变体

1. 寻找左边界

def left_bound(arr, target):left, right = 0, len(arr)while left < right:mid = left + (right - left) // 2if arr[mid] < target:left = mid + 1else:right = midreturn left

2. 寻找右边界

def right_bound(arr, target):left, right = 0, len(arr)while left < right:mid = left + (right - left) // 2if arr[mid <= target:left = mid + 1else:right = midreturn 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)不稳定

快速排序实现

def quick_sort(arr):if len(arr) <= 1:return arrpivot = 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)

归并排序实现

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

8.3 位运算

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

常用技巧

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

经典题目

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

def single_number(nums):result = 0for num in nums:result ^= num  # 异或return result

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

2. 位1的个数(LeetCode 191)

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

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

def reverse_bits(n):result = 0for _ in range(32):result = (result << 1) | (n & 1)n >>= 1return 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》 - 面试算法题的经典参考书
    作者: 何海涛
http://www.dtcms.com/a/574517.html

相关文章:

  • Web开发身份认证技术解析
  • 做汽车网站怎么挣钱吗深圳网站建设公司好
  • 网站建设素材网页apache 创建网站
  • 虚函数指针与虚函数表:C++多态的实现奥秘
  • 小说类网站怎么做建设推广营销型网站应该注意什么
  • ubuntu 安装 SRS (Simple RTMP Server) 是一个开源的流媒体服务器
  • 怎么自己设计网站外贸公司 网站
  • 【仓颉纪元】仓颉鸿蒙应用深度开发:待办事项 App 全流程实战
  • 领英被封?账号受限该怎么处理?
  • 信誉好的镇江网站建设网站备案名称中国开头
  • 【C语言】localtime和localtime_r;strftime和strftime_l
  • 扁平化设计网站代码打开网站后直接做跳转
  • Go 语言依赖注入实战指南:从基础到高级实践
  • 全场景自动化 Replay 技术:金仓 KReplay 如何攻克数据库迁移 “难验证“ 难题
  • 阳新县建设局网站win2008系统asp网站建设
  • 网站域名分几种新东方雅思培训机构官网
  • 网站怎么样做不违规学科基地网站建设
  • MySQL-4-视图和索引
  • 电脑被捆绑软件缠上?3 步根治卡顿弹窗~
  • Linux时间处理与系统时间管理详解
  • 上饶建设局网站开封到濮阳
  • 织梦网站动态华为云自助建站
  • RocketMQ集群核心概念 生产者端的负载均衡
  • 做恒生指数看什么网站贵州网站优化
  • 百度搜索引擎平台seo全称英文怎么说
  • 黑马点评学习笔记07(缓存工具封装)
  • BLDC电流采样的四种方式
  • 物流行业网站建设市场分析品牌策划方案案例
  • 高校对网站建设的重视郑州建设电商网站
  • 网站后台管理代码凡科h5在线制作