chore: import zh skill code-mentor
This commit is contained in:
@@ -0,0 +1,731 @@
|
||||
# 常见算法模式
|
||||
|
||||
本参考涵盖编程面试和实际解决问题中最常用的算法模式。理解这些模式有助于你识别应对陌生问题时应采用哪种方法。
|
||||
|
||||
---
|
||||
|
||||
## 模式 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)
|
||||
|
||||
---
|
||||
|
||||
## 模式 12:Top 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
|
||||
- 动态规划 + 记忆化
|
||||
- 回溯 + 剪枝
|
||||
Reference in New Issue
Block a user