# 常见算法模式 本参考涵盖编程面试和实际解决问题中最常用的算法模式。理解这些模式有助于你识别应对陌生问题时应采用哪种方法。 --- ## 模式 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 - 动态规划 + 记忆化 - 回溯 + 剪枝