Files
skillhub-117-code-mentor/references/algorithms/common-patterns.md
T
2026-07-13 21:36:32 +08:00

732 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 常见算法模式
本参考涵盖编程面试和实际解决问题中最常用的算法模式。理解这些模式有助于你识别应对陌生问题时应采用哪种方法。
---
## 模式 1:双指针
**适用场景**:需要寻找数对、三元组或从两端处理元素的数组或字符串问题。
**何时使用**
- 在有序数组中寻找和为目标的数对
- 原地反转数组或字符串
- 从有序数组中移除重复项
- 盛最多水的容器类问题
**示例问题**
- 两数之和(有序数组)
- 验证回文串
- 盛最多水的容器
- 三数之和
**实现(Python**
```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**
```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 的最大/最小子数组和
- 无重复字符的最长子串
- 在字符串中查找所有字母异位词
- 最小覆盖子串
**类型**
1. **固定大小窗口**:窗口大小恒定(例如大小为 k 的最大和)
2. **可变大小窗口**:窗口根据条件增大或缩小
**示例问题**
- 大小为 K 的最大子数组和
- 无重复字符的最长子串
- 最小覆盖子串
- 字符串中的排列
**实现(Python)—— 固定窗口**
```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)—— 可变窗口**
```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**
```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**
```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**
```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**
```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**
```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**
```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(深度优先搜索)
**适用场景**:基于路径的树问题,递归树遍历。
**何时使用**
- 查找从根到叶的所有路径
- 路径数字之和
- 给定和的路径
- 统计和为某值的路径数
- 树的直径
**类型**
1. **前序遍历**:根 → 左 → 右
2. **中序遍历**:左 → 根 → 右
3. **后序遍历**:左 → 右 → 根
**示例问题**
- 二叉树路径
- 路径总和
- 求根到叶节点数字之和
- 二叉树的直径
**实现(Python**
```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**
```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**
```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:二分查找
**适用场景**:在有序数组或搜索空间中查找,寻找边界。
**何时使用**
- 在有序数组中查找
- 查找第一个/最后一个出现位置
- 在旋转有序数组中查找
- 寻找峰值元素
- 在二维矩阵中查找
**模板**
```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 # 未找到
```
**时间复杂度**O(log n)
**空间复杂度**O(1)
---
## 模式 12Top K 元素
**适用场景**:查找 k 个最大/最小元素,k 个最频繁元素。
**何时使用**
- K 个最大/最小元素
- K 个最近的点
- K 个最频繁的元素
- 按频率对字符排序
**实现(Python**
```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**
```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 背包
**模板(记忆化)**
```python
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:动态规划(自底向上)
**适用场景**:与自顶向下相同,但采用迭代方式(通常更高效)。
**模板(表格法)**
```python
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]
```
**空间优化**(以斐波那契为例):
```python
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
```
---
## 如何选择正确的模式
问自己以下几个问题:
1. **输入结构是什么?**
- 有序数组 → 二分查找、双指针
- 链表 → 快慢指针、原地反转
- 树 → BFS、DFS
- 区间 → 合并区间
2. **我在寻找什么?**
- 子数组/子串 → 滑动窗口
- 数对/三元组 → 双指针
- 所有组合 → 回溯
- 带选择的最优解 → 动态规划
- Top k 个元素 → 堆
3. **是否存在约束条件?**
- 数字范围在 [1, n] → 循环排序
- 需要中位数 → 双堆
- 原地修改 → 双指针、循环排序
4. **时间复杂度要求是什么?**
- O(log n) → 二分查找
- O(n) → 双指针、滑动窗口、哈希表
- O(n log n) → 排序、堆
- 可以接受指数级? → 回溯、递归
---
**练习策略**
1. 每次掌握一种模式
2. 每个模式解决 5-10 道题
3. 在新问题中识别模式
4. 组合模式解决复杂问题
**常见模式组合**
- 双指针 + 滑动窗口
- 二分查找 + DFS
- 动态规划 + 记忆化
- 回溯 + 剪枝