16 KiB
常见算法模式
本参考涵盖编程面试和实际解决问题中最常用的算法模式。理解这些模式有助于你识别应对陌生问题时应采用哪种方法。
模式 1:双指针
适用场景:需要寻找数对、三元组或从两端处理元素的数组或字符串问题。
何时使用:
- 在有序数组中寻找和为目标的数对
- 原地反转数组或字符串
- 从有序数组中移除重复项
- 盛最多水的容器类问题
示例问题:
- 两数之和(有序数组)
- 验证回文串
- 盛最多水的容器
- 三数之和
实现(Python):
def two_sum_sorted(arr, target):
"""在有序数组中寻找两个数使其和等于目标值。"""
left, right = 0, len(arr) - 1
while left < right:
current_sum = arr[left] + arr[right]
if current_sum == target:
return [left, right]
elif current_sum < target:
left += 1 # 需要更大的和
else:
right -= 1 # 需要更小的和
return None # 未找到解
实现(JavaScript):
function twoSumSorted(arr, target) {
let left = 0, right = arr.length - 1;
while (left < right) {
const currentSum = arr[left] + arr[right];
if (currentSum === target) {
return [left, right];
} else if (currentSum < target) {
left++;
} else {
right--;
}
}
return null;
}
时间复杂度:O(n) —— 单次遍历数组 空间复杂度:O(1) —— 仅两个指针
模式 2:滑动窗口
适用场景:涉及子数组或子串的问题,需要寻找最优窗口大小或跟踪连续序列中的元素。
何时使用:
- 大小为 k 的最大/最小子数组和
- 无重复字符的最长子串
- 在字符串中查找所有字母异位词
- 最小覆盖子串
类型:
- 固定大小窗口:窗口大小恒定(例如大小为 k 的最大和)
- 可变大小窗口:窗口根据条件增大或缩小
示例问题:
- 大小为 K 的最大子数组和
- 无重复字符的最长子串
- 最小覆盖子串
- 字符串中的排列
实现(Python)—— 固定窗口:
def max_sum_subarray(arr, k):
"""找出大小为 k 的任意子数组的最大和。"""
if len(arr) < k:
return None
# 计算第一个窗口的和
window_sum = sum(arr[:k])
max_sum = window_sum
# 滑动窗口
for i in range(k, len(arr)):
window_sum = window_sum - arr[i - k] + arr[i]
max_sum = max(max_sum, window_sum)
return max_sum
实现(JavaScript)—— 可变窗口:
function lengthOfLongestSubstring(s) {
const seen = new Set();
let left = 0;
let maxLength = 0;
for (let right = 0; right < s.length; right++) {
// 收缩窗口直到无重复
while (seen.has(s[right])) {
seen.delete(s[left]);
left++;
}
seen.add(s[right]);
maxLength = Math.max(maxLength, right - left + 1);
}
return maxLength;
}
时间复杂度:O(n) —— 每个元素最多被访问两次 空间复杂度:固定窗口为 O(k),可变窗口(含哈希集合)为 O(n)
模式 3:快慢指针(Floyd 环检测)
适用场景:链表问题,尤其是环检测和寻找中间元素。
何时使用:
- 检测链表中的环
- 寻找链表的中点
- 寻找环的起点
- 判断一个数是否为快乐数
示例问题:
- 环形链表
- 快乐数
- 寻找链表的中间节点
- 环起点检测
实现(Python):
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
def has_cycle(head):
"""检测链表是否有环。"""
if not head:
return False
slow = fast = head
while fast and fast.next:
slow = slow.next # 移动 1 步
fast = fast.next.next # 移动 2 步
if slow == fast:
return True # 检测到环
return False
时间复杂度:O(n) 空间复杂度:O(1)
模式 4:合并区间
适用场景:处理重叠区间、调度或范围的问题。
何时使用:
- 合并重叠区间
- 插入区间
- 会议室问题
- 区间交集
示例问题:
- 合并区间
- 插入区间
- 会议室 II
- 区间列表的交集
实现(Python):
def merge_intervals(intervals):
"""合并重叠区间。"""
if not intervals:
return []
# 按开始时间排序
intervals.sort(key=lambda x: x[0])
merged = [intervals[0]]
for current in intervals[1:]:
last_merged = merged[-1]
if current[0] <= last_merged[1]:
# 重叠 —— 合并
merged[-1] = [last_merged[0], max(last_merged[1], current[1])]
else:
# 不重叠 —— 添加新区间
merged.append(current)
return merged
时间复杂度:O(n log n) —— 因排序导致 空间复杂度:O(n) —— 输出空间
模式 5:循环排序
适用场景:数组中包含给定范围内(通常为 1 到 n)的数字的问题。
何时使用:
- 查找缺失/重复的数字
- 查找所有缺失的数字
- 查找损坏数对
- 包含 1 到 n 数字的数组
示例问题:
- 寻找缺失数字
- 寻找所有缺失数字
- 寻找重复数字
- 寻找损坏数对
实现(Python):
def cyclic_sort(nums):
"""对范围为 1 到 n 的数组进行排序。"""
i = 0
while i < len(nums):
correct_index = nums[i] - 1
if nums[i] != nums[correct_index]:
# 交换到正确位置
nums[i], nums[correct_index] = nums[correct_index], nums[i]
else:
i += 1
return nums
def find_missing_number(nums):
"""在 [0, n] 范围内查找缺失的数字。"""
n = len(nums)
i = 0
# 循环排序
while i < n:
correct_index = nums[i]
if nums[i] < n and nums[i] != nums[correct_index]:
nums[i], nums[correct_index] = nums[correct_index], nums[i]
else:
i += 1
# 查找缺失
for i in range(n):
if nums[i] != i:
return i
return n
时间复杂度:O(n) 空间复杂度:O(1)
模式 6:链表原地反转
适用场景:在不使用额外空间的情况下反转链表或链表的一部分。
何时使用:
- 反转整个链表
- 反转从位置 m 到 n 的子链表
- 按 k 个一组反转
- 回文链表检测
示例问题:
- 反转链表
- 反转链表 II
- K 个一组翻转链表
实现(Python):
def reverse_linked_list(head):
"""原地反转链表。"""
prev = None
current = head
while current:
next_node = current.next # 保存下一个节点
current.next = prev # 反转指针
prev = current # 向前移动 prev
current = next_node # 向前移动 current
return prev # 新的头节点
实现(JavaScript):
function reverseLinkedList(head) {
let prev = null;
let current = head;
while (current !== null) {
const nextNode = current.next;
current.next = prev;
prev = current;
current = nextNode;
}
return prev;
}
时间复杂度:O(n) 空间复杂度:O(1)
模式 7:树 BFS(广度优先搜索)
适用场景:树的层序遍历,查找层级特定信息。
何时使用:
- 层序遍历
- 查找最小深度
- 锯齿形层序遍历
- 连接层序兄弟节点
- 树的右视图
示例问题:
- 二叉树的层序遍历
- 二叉树的锯齿形遍历
- 二叉树的最小深度
- 连接层序兄弟节点
实现(Python):
from collections import deque
def level_order_traversal(root):
"""BFS 遍历,返回层级列表。"""
if not root:
return []
result = []
queue = deque([root])
while queue:
level_size = len(queue)
current_level = []
for _ in range(level_size):
node = queue.popleft()
current_level.append(node.val)
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
result.append(current_level)
return result
时间复杂度:O(n) 空间复杂度:O(n) —— 队列空间
模式 8:树 DFS(深度优先搜索)
适用场景:基于路径的树问题,递归树遍历。
何时使用:
- 查找从根到叶的所有路径
- 路径数字之和
- 给定和的路径
- 统计和为某值的路径数
- 树的直径
类型:
- 前序遍历:根 → 左 → 右
- 中序遍历:左 → 根 → 右
- 后序遍历:左 → 右 → 根
示例问题:
- 二叉树路径
- 路径总和
- 求根到叶节点数字之和
- 二叉树的直径
实现(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
# 递归 DFS
remaining_sum = target_sum - root.val
return (has_path_sum(root.left, remaining_sum) or
has_path_sum(root.right, remaining_sum))
时间复杂度:O(n) 空间复杂度:O(h),其中 h 为树高(递归栈)
模式 9:双堆
适用场景:需要查找中位数或将元素分为两半的问题。
何时使用:
- 从数据流中找中位数
- 滑动窗口中位数
- IPO(最大化资本)
结构:
- 最大堆:存储较小的一半数字
- 最小堆:存储较大的一半数字
- 中位数为最大堆的最大值或两堆堆顶的平均值
实现(Python):
import heapq
class MedianFinder:
def __init__(self):
self.max_heap = [] # 较小的一半(取反实现最大堆)
self.min_heap = [] # 较大的一半
def add_num(self, num):
# 先加入最大堆
heapq.heappush(self.max_heap, -num)
# 平衡:将最大堆的最大值移到最小堆
heapq.heappush(self.min_heap, -heapq.heappop(self.max_heap))
# 确保最大堆元素个数等于或多于最小堆
if len(self.max_heap) < len(self.min_heap):
heapq.heappush(self.max_heap, -heapq.heappop(self.min_heap))
def find_median(self):
if len(self.max_heap) > len(self.min_heap):
return -self.max_heap[0]
return (-self.max_heap[0] + self.min_heap[0]) / 2
时间复杂度:插入 O(log n),查找中位数 O(1) 空间复杂度:O(n)
模式 10:子集(回溯)
适用场景:需要生成所有组合、排列或子集的问题。
何时使用:
- 生成所有子集/幂集
- 排列
- 组合
- 字母大小写全排列
示例问题:
- 子集
- 排列
- 组合
- 括号生成
实现(Python):
def subsets(nums):
"""使用回溯生成所有子集。"""
result = []
def backtrack(start, current):
# 添加当前子集
result.append(current[:])
# 探索后续元素
for i in range(start, len(nums)):
current.append(nums[i])
backtrack(i + 1, current)
current.pop() # 回溯
backtrack(0, [])
return result
时间复杂度:O(2^n) —— 指数级 空间复杂度:O(n) —— 递归深度
模式 11:二分查找
适用场景:在有序数组或搜索空间中查找,寻找边界。
何时使用:
- 在有序数组中查找
- 查找第一个/最后一个出现位置
- 在旋转有序数组中查找
- 寻找峰值元素
- 在二维矩阵中查找
模板:
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 # 未找到
时间复杂度:O(log n) 空间复杂度:O(1)
模式 12:Top K 元素
适用场景:查找 k 个最大/最小元素,k 个最频繁元素。
何时使用:
- K 个最大/最小元素
- K 个最近的点
- K 个最频繁的元素
- 按频率对字符排序
实现(Python):
import heapq
def k_largest_elements(nums, k):
"""使用最小堆查找 k 个最大元素。"""
# 维护大小为 k 的最小堆
min_heap = []
for num in nums:
heapq.heappush(min_heap, num)
if len(min_heap) > k:
heapq.heappop(min_heap)
return min_heap
时间复杂度:O(n log k) 空间复杂度:O(k)
模式 13:改进版二分查找
适用场景:针对复杂场景的二分查找变体。
何时使用:
- 在旋转有序数组中查找
- 在旋转有序数组中查找最小值
- 在无限有序数组中查找
- 查找范围(第一个和最后一个位置)
实现(Python):
def search_rotated_array(nums, target):
"""在旋转有序数组中查找目标值。"""
left, right = 0, len(nums) - 1
while left <= right:
mid = left + (right - left) // 2
if nums[mid] == target:
return mid
# 判断哪一半是有序的
if nums[left] <= nums[mid]: # 左半有序
if nums[left] <= target < nums[mid]:
right = mid - 1
else:
left = mid + 1
else: # 右半有序
if nums[mid] < target <= nums[right]:
left = mid + 1
else:
right = mid - 1
return -1
模式 14:动态规划(自顶向下)
适用场景:具有重叠子问题的最优化问题。
何时使用:
- 斐波那契数列、爬楼梯
- 打家劫舍
- 零钱兑换
- 最长公共子序列
- 0/1 背包
模板(记忆化):
def fibonacci(n, memo={}):
"""使用记忆化计算第 n 个斐波那契数。"""
if n in memo:
return memo[n]
if n <= 1:
return n
memo[n] = fibonacci(n - 1, memo) + fibonacci(n - 2, memo)
return memo[n]
时间复杂度:取决于具体问题(通常为 O(n) 或 O(n²)) 空间复杂度:O(n) —— 记忆化加递归栈
模式 15:动态规划(自底向上)
适用场景:与自顶向下相同,但采用迭代方式(通常更高效)。
模板(表格法):
def fibonacci_dp(n):
"""使用自底向上 DP 计算第 n 个斐波那契数。"""
if n <= 1:
return n
dp = [0] * (n + 1)
dp[1] = 1
for i in range(2, n + 1):
dp[i] = dp[i - 1] + dp[i - 2]
return dp[n]
空间优化(以斐波那契为例):
def fibonacci_optimized(n):
"""空间优化的斐波那契计算。"""
if n <= 1:
return n
prev2, prev1 = 0, 1
for _ in range(2, n + 1):
current = prev1 + prev2
prev2, prev1 = prev1, current
return prev1
如何选择正确的模式
问自己以下几个问题:
-
输入结构是什么?
- 有序数组 → 二分查找、双指针
- 链表 → 快慢指针、原地反转
- 树 → BFS、DFS
- 区间 → 合并区间
-
我在寻找什么?
- 子数组/子串 → 滑动窗口
- 数对/三元组 → 双指针
- 所有组合 → 回溯
- 带选择的最优解 → 动态规划
- Top k 个元素 → 堆
-
是否存在约束条件?
- 数字范围在 [1, n] → 循环排序
- 需要中位数 → 双堆
- 原地修改 → 双指针、循环排序
-
时间复杂度要求是什么?
- O(log n) → 二分查找
- O(n) → 双指针、滑动窗口、哈希表
- O(n log n) → 排序、堆
- 可以接受指数级? → 回溯、递归
练习策略:
- 每次掌握一种模式
- 每个模式解决 5-10 道题
- 在新问题中识别模式
- 组合模式解决复杂问题
常见模式组合:
- 双指针 + 滑动窗口
- 二分查找 + DFS
- 动态规划 + 记忆化
- 回溯 + 剪枝