
LeetCode 刷题刷到怀疑人生通常不是题量不够而是没有把题目背后反复出现的模式抽出来。今天不列题单不推付费课程直接整理 8 种高频解题模式。每一种都给出“什么时候用 模板代码 复杂度 典型题”按这套框架刷效率会明显不一样。先说结论大部分 LeetCode 中等题和一部分困难题都能归入下面 8 种模式中的一种或几种。拿到一道新题先按“求什么、输入长什么样、数据范围多少”去匹配模式再套模板写边界条件比从零硬做要稳得多。这次的主题就是“用套路解题”而不是“每道题都重新发明一次解法”。这 8 种模式覆盖面有多广看一套粗略统计在 LeetCode 热题 100 和剑指 Offer 里滑动窗口、双指针、二分、前缀和、DFS/BFS、回溯、动态规划这 8 类题目占了七成以上。也就是说掌握它们基本就掌握了算法面试的主干。下面逐个展开。每个模式分四部分适用场景、识别信号、模板代码、踩坑提醒。建议先看模板再拿典型题验证最后回到“识别信号”确认自己能不能看到题就能想到对应模式。1. 核心解题模式速览先看一张模式总览表后面再逐个展开。模式典型特征适用题型常见题号滑动窗口连续子数组/子串求最值、计数子串问题、最小覆盖子串、无重复字符3、76、209双指针有序数组、链表、回文检测两数之和、三数之和、环检测15、167、283二分查找有序数组或在答案值域上二分查找边界、吃香蕉、分割数组34、704、875前缀和 哈希连续子数组和、路径和、目标和和为 K 的子数组、矩阵区域和437、560、523哈希表/频率统计去重、计数、配对、是否出现两数之和、字母异位词、最长连续序列1、128、242DFS / BFS图的遍历、扩散模型、层级问题岛屿数量、二叉树路径、单词接龙102、200、994回溯法排列、组合、子集、棋盘搜索全排列、组合总和、N 皇后46、79、131动态规划最值、方案数、子序列、状态转移零钱兑换、最长递增子序列、打家劫舍198、300、322看到题以后审题三件事是连续子结构还是离散子序列是一维线性还是二维平面?需要全组合还是任意一个可行解这个判断比急着写代码更重要。2. 滑动窗口模式2.1 适用场景与识别信号滑动窗口解决的是“连续子数组/连续子串”问题特征是题目要求里带“连续、子数组、子串”等关键词并且是求一个最优值或计数。比如“找到最长的无重复字符子串”“求和至少为 S 的最短子数组”“找出包含所有字符的最短子串”。它的核心思路是维护 left 和 right 两个指针right 负责向外扩展进入新元素当窗口内条件不满足时left 负责收缩并更新状态。循环一直到 right 越界。整个过程只会把每个元素进一次、出一次所以复杂度是 O(n)这是它相对暴力枚举的最大优势。2.2 滑动窗口模板public int slidingWindow(int[] nums) { int n nums.length; int left 0; int ans 0; MapInteger, Integer window new HashMap(); for (int right 0; right n; right) { // 将 nums[right] 加入窗口 window.merge(nums[right], 1, Integer::sum); // 条件不满足时收缩左侧窗口 while (window.size() 条件) { window.merge(nums[left], -1, Integer::sum); if (window.get(nums[left]) 0) { window.remove(nums[left]); } left; } // 更新结果 ans Math.max(ans, right - left 1); } return ans; }模板里的“条件”要根据题目改写。如果是子串问题数组换成字符串即可维护的窗口状态可能是字符频次、不同字符个数、子串和等。2.3 典型题目实战LeetCode 3. 无重复字符的最长子串。用 HashMap 存储字符出现的次数right 不断扩展一旦窗口内某个字符出现次数大于 1就 left 向右收缩直到重复字符消除。每次记录right - left 1的最大值。这里窗口维护的是“字符频率”需要注意收缩时把移出的字符频率减一减到 0 则从 map 中移除否则后续判断会出错。LeetCode 76. 最小覆盖子串。需要两个哈希表一个记录 t 中每个字符的需求数一个记录当前窗口中的字符数。当窗口中的字符已经满足 t 的所有字符需求时尝试移动 left 缩短窗口并比较更新答案。这里的“条件判断”比无重复字符更复杂因为需要动态统计“已满足的字符种类数”而不是简单判断窗口大小。LeetCode 209. 长度最小的子数组。窗口内维护和和大于等于 target 时就收缩左边界并记录最小长度。遇到正整数数组这个解法是线性的。有一个容易出错的点没有满足条件的子数组时要返回 0 而不是返回一个默认的极大值。踩坑提醒滑动窗口并不是所有子数组问题都能解决。如果数组中存在负数窗口内“越长和越大”的前提不成立这时滑动窗口会失效。数组是正整数、字符匹配这类单调场景才适合用滑动窗口。看到负数优先想前缀和。3. 双指针模式3.1 适用场景与识别信号双指针不是单一的算法而是一簇技巧。最常见的三种左右指针有序数组里left 在开头right 在末尾根据 sum 与 target 的大小关系调整指针。快慢指针链表/数组中找环、找中点。同向指针去重、移动零、寻找第 N 个某种元素。识别信号也很直接数组是有序的或者要寻找两个/多个元素之间的关系比如“两数之和”“三数之和”“平方数之和”。链表题里出现“环”“倒数第 k 个节点”“找到中间节点”这些关键词也可以直接进入双指针模式。3.2 双指针模板// 左右指针适用于有序数组 public int[] twoSum(int[] nums, int target) { int left 0, right nums.length - 1; while (left right) { int sum nums[left] nums[right]; if (sum target) { return new int[]{left, right}; } else if (sum target) { left; } else { right--; } } return new int[]{-1, -1}; }// 快慢指针适用于环检测 public boolean hasCycle(ListNode head) { ListNode slow head, fast head; while (fast ! null fast.next ! null) { slow slow.next; fast fast.next.next; if (slow fast) { return true; } } return false; }三数之和稍微进阶一点先排序固定一个数剩下两个数用左右指针夹逼。重点在于去重固定数和左右指针移动时都要跳过重复值否则结果会包含重复三元组。看过很多人在这个题上死磕去重其实代码没多难关键是要在 while 循环里同时处理三处去重。4. 二分查找模式4.1 适用场景与识别信号一说二分就只想到有序数组这是最常见的误区。二分查找其实是一种在单调函数上搜索边界的技术。数组有序只是它的一个应用并不是全部。更泛化的识别信号是题目要你求“最小化最大值”或“最大化的最小值”典型的比如“在 D 天内送完包裹的能力”“爱吃香蕉的狒狒每小时最少吃多少根”“分割数组的最大值最小”。这类题的数据范围往往较大n 可以到 10^5 甚至 10^9暴力枚举通常超时。看到数据范围大、并且判断答案可行性比较容易的就要想到二分答案。4.2 二分答案模板public int minEatingSpeed(int[] piles, int h) { int left 1, right 0; for (int p : piles) { right Math.max(right, p); } while (left right) { int mid left (right - left) / 2; if (canFinish(piles, h, mid)) { right mid; // 找左边界答案可能还能更小 } else { left mid 1; } } return left; } private boolean canFinish(int[] piles, int h, int speed) { int hours 0; for (int p : piles) { hours (p speed - 1) / speed; // 上取整 } return hours h; }注意这里使用的是left right配合right mid的写法它最终收敛到满足条件的最小值。很多模板写left right在找边界时容易死循环建议把两种边界类型分别固定下来找左边界用一种写法找右边界用另一种写法别混用。4.3 二分查找的隐藏用法LeetCode 875“爱吃香蕉的狒狒”就是典型的二分答案。每小时吃的香蕉数speed范围是 1 到 max(piles)能吃完全部香蕉与否是一个随 speed 单调变化的函数所以可以对 speed 做二分。判断函数 check 的复杂度是 O(n)二分的复杂度是 O(log(maxPile))总体很快。还有一种用法是二分答案在浮点数上比如“求平方根”“找出两个正序数组的中位数”。浮点二分需要设定精度阈值通常到 1e-7 就够了。做题时先用整数二分想清楚边界再改写浮点会顺畅很多。5. 前缀和 哈希表模式5.1 适用场景与识别信号前缀和解决的是“连续子数组和”的查询问题。为了把复杂度进一步降低通常配合哈希表记录“出现过的前缀和次数”。“连续子数组的和等于 k”“连续子数组的和能被 k 整除”“路径和等于 k”这几类问题都适用。识别信号关键是两个词连续区间和。暴力两层循环求区间和的复杂度是 O(n^2)前缀和可以把单次查询优化到 O(1)再用哈希表把“找目标前缀和”的过程下降到 O(n)。5.2 前缀和模板public int subarraySum(int[] nums, int k) { int count 0; int prefixSum 0; MapInteger, Integer map new HashMap(); map.put(0, 1); // 初始前缀和为0出现一次 for (int num : nums) { prefixSum num; // 当前前缀和 - 目标k如果之前出现过说明中间这一段的和为k int target prefixSum - k; count map.getOrDefault(target, 0); map.put(prefixSum, map.getOrDefault(prefixSum, 0) 1); } return count; }这段代码的核心逻辑是子数组和等于 k可以转换成两个前缀和之差等于 k。只要遍历一遍用 map 记录之前出现过的前缀和次数就能在线性时间内统计所有合法子数组个数。LeetCode 560 就是这个模板的原型题。5.3 从子数组扩展到树路径LeetCode 437“路径总和 III”是前缀和从一维扩展到树形结构的代表。树上的路径可以看作从根节点往下的一条连续“前缀路径”在 DFS 过程中不断累加路径和同时把当前前缀和存入哈希表。离开一个节点时要回溯把 map 中的计数减掉防止跨分支的错误累计。踩坑提醒前缀和如果全程递增滑窗可以解决如果数组里有负数一定优先用前缀和。LeetCode 有一类题专门用负数来阻止你套滑窗模板比如“和为 K 的子数组”如果窗口内和为 k 的条件被负数干扰left 收缩的单调性就丢了。6. DFS / BFS 搜索模式6.1 适用场景与识别信号DFS 和 BFS 解决的是“图/树的遍历”问题。判别信号分两类求“连通分量个数”“岛屿数量”“可达区域”用 DFS 会比较方便。求“最短路径”“最小步数”“按层扩散”用 BFS因为 BFS 天然按层遍历第一次到达终点的层数就是最短路径。二维网格中大量“岛屿类”题目都是 DFS/BFS 的变形特征是迷宫、0-1 方格、染色扩散等。二叉树题里“层序遍历”“最小深度”“最大深度”则是直接套 DFS/BFS 很自然的题目。6.2 岛屿类 DFS 模板public int numIslands(char[][] grid) { if (grid null || grid.length 0) { return 0; } int rows grid.length; int cols grid[0].length; int count 0; for (int i 0; i rows; i) { for (int j 0; j cols; j) { if (grid[i][j] 1) { count; dfs(grid, i, j); } } } return count; } private void dfs(char[][] grid, int i, int j) { if (i 0 || j 0 || i grid.length || j grid[0].length || grid[i][j] 0) { return; } grid[i][j] 0; // 标记访问过防止重复遍历 dfs(grid, i 1, j); dfs(grid, i - 1, j); dfs(grid, i, j 1); dfs(grid, i, j - 1); }DFS 的代码结构非常固定递归出口判断越界或不满足条件然后向四个方向递归。唯一的改动点是状态去重有些题允许回溯有些题直接标记即可。岛屿类使用原地标记不额外用 visited 数组节省空间。6.3 BFS 模板与最短路径BFS 在“最短路径”问题上更自然因为它在图中逐层扫描。下面这个模板配合队列和 visited 数组可以解决大部分网格 BFS 类问题public int bfs(char[][] grid) { int rows grid.length, cols grid[0].length; int[][] dirs {{1,0}, {-1,0}, {0,1}, {0,-1}}; Queueint[] queue new LinkedList(); int steps 0; while (!queue.isEmpty()) { int size queue.size(); for (int i 0; i size; i) { int[] cur queue.poll(); for (int[] dir : dirs) { int nx cur[0] dir[0]; int ny cur[1] dir[1]; // 越界或访问判断 // queue.offer(new int[]{nx, ny}); } } steps; } return steps; }从网格搜索扩展到树的层序遍历思路是一样的用队列保存每一层的节点。LeetCode 102“二叉树的层序遍历”是树上的 BFS 基础题熟练了之后单词接龙、打开转盘锁这类“状态作为节点”的 BFS 题就更容易理解。7. 回溯法模式7.1 适用场景与识别信号回溯解决的问题是“枚举所有可能解”特征是题目要求里带“所有组合”“所有排列”“所有路径”“是否存在一个可行解”。本质上它是在一棵决策树上做 DFS每次走一步就做一次选择不满足条件时撤销选择回到上一层继续尝试。常见题型有全排列、子集、组合总和、括号生成、单词搜索、N 皇后、数独。刷题时一旦看到“返回所有可能结果”或“能否到达目标”的字眼回溯是首要候选。7.2 回溯模板public ListListInteger permute(int[] nums) { ListListInteger res new ArrayList(); backtrack(nums, new boolean[nums.length], new ArrayList(), res); return res; } private void backtrack(int[] nums, boolean[] used, ListInteger path, ListListInteger res) { if (path.size() nums.length) { res.add(new ArrayList(path)); return; } for (int i 0; i nums.length; i) { if (used[i]) { continue; } used[i] true; path.add(nums[i]); backtrack(nums, used, path, res); path.remove(path.size() - 1); // 撤销选择 used[i] false; } }这个模板能跑通全排列子集和组合类型则通常在 for 循环里传startIndex避免选择之前的元素产生重复组合。7.3 剪枝是性能核心回溯法写出来不难难的在于剪枝。剪枝可以分成两类可行性剪枝当前路径已经不可能形成解立即 return。比如组合总和里当前路径的和已经超过 target就停止继续向下递归。排列去重剪枝先对数组排序然后判断i 0 nums[i] nums[i - 1] !used[i - 1]就 continue。这个是处理包含重复元素的全排列/子集的标准写法。注意当写“同一层去重”时要保留“同一路径下重复元素可以用”的语义两者差异体现在used[i-1]的真假判断上这是回溯题最容易错的地方。LeetCode 79“单词搜索”则是回溯在二维网格上的应用它需要在矩阵里尝试四个方向每个位置只能经过一次所以用 visited 标记并回溯取消标记。这类题 DFS 和回溯没有本质区别关键是理解“什么时候撤销选择”DFS 只关心是否可达回溯关心的是把所有不同的尝试路径都走完。8. 动态规划模式8.1 适用场景与识别信号动态规划是 LeetCode 中等题的半壁江山。典型特征可以归纳为求解最值、方案总数、True/False 可达性并且决策可以拆成一个个子状态逐步递推。经典例子爬楼梯、打家劫舍、零钱兑换、最长公共子序列、最长递增子序列。做动态规划的核心步骤是五步定义状态dp[i]或dp[i][j]的含义。找到状态转移方程即 dp[i] 怎么从前面的 dp 值推导出来。初始化边界条件比如 dp[0] 是多少。确定遍历顺序。返回哪个 dp 值。其中第 1 步的状态定义是最难的部分。很多人写到困难题不会做往往不是转移方程不会列而是状态本身定义错了维度或者含义太模糊。比如“最多 k 次交易”需要dp[i][k][持有]三维状态“编辑距离”需要维护两个字符串前缀之间的 dp。8.2 一维 DP 模板打家劫舍LeetCode 198 是一个很适合入门的一维 DP 问题。每家都有金额不能偷相邻两家。状态定义是dp[i]表示到第 i 家时能偷到的最大金额。转移方程dp[i] max(dp[i-1], dp[i-2] nums[i])含义是“不偷当前这家保持上一家的最大值”与“偷当前这家并且跳过上一家”的最大值。初始化时dp[0]nums[0]dp[1]max(nums[0], nums[1])。实际操作时还能用滚动变量把空间复杂度从 O(n) 降到 O(1)。很多一维 DP 问题都可以这样压缩空间但做题时先写二维再改滚动数组不容易出错。8.3 二维 DP 模板最长递增子序列 / 编辑距离如果二维 dp 状态含义是“处理到 i 和 j 时的答案”那么转移方程通常只需要比较最后一个元素是否相同。以最长递增子序列为例// O(n^2) 解法dp[i] 表示以 nums[i] 结尾的最长递增子序列长度 public int lengthOfLIS(int[] nums) { int n nums.length; int[] dp new int[n]; Arrays.fill(dp, 1); int ans 1; for (int i 1; i n; i) { for (int j 0; j i; j) { if (nums[j] nums[i]) { dp[i] Math.max(dp[i], dp[j] 1); } } ans Math.max(ans, dp[i]); } return ans; }最终答案是 dp 数组里的最大值不是 dp[n-1]。动态规划最花时间的不是写代码而是验证状态转移方程。建议每道新题先手工推一个小规模用例依次把 dp 表格填一遍。如果状态转移方程连小用例都不能解释通大概率是状态定义一开始就错了。9. 其他高频模式和综合建议9.1 哈希表优化哈希表不是独立的一类算法而是贯穿在大部分模式里做“时间换空间”的辅助工具。两数之和用哈希表把查找从 O(n) 降到 O(1)字母异位词分组用哈希表存储排序后的字符串分组最长连续序列用 HashSet 去重后只从序列起点开始遍历。它在模板里的位置通常是先想清楚暴力解法里每次循环哪一步在重复查找然后把“已经计算过的值/前缀/状态”存入哈希表。这个优化思路适用范围极广从两数之和到前缀和到两两配对都适用。9.2 贪心算法贪心模式虽然单独看比重没有 DP 那么大但在某些专题里非常重要。特征是局部最优可以推出全局最优不需要动态规划那样枚举所有状态。经典应用区间调度终点越早越优、跳跃游戏、分发饼干、种花问题。识别信号比较难因为它不像 DP 有清晰的状态模板。如果题目只需要返回“能不能到达终点”而不是“所有方案”可以先想贪心再反证是否成立。9.3 选择难度的一种“最少 50 题”路径如果时间紧张可刷 50 题把这 8 个模式走一遍每个模式挑 2 到 3 道基础题 1 道进阶题。比如滑动窗口刷 3、209、76。双指针刷 15、167、283。二分刷 704、875、410。前缀和刷 560、437。DFS/BFS 刷 200、102、994。回溯刷 46、79、131。DP 刷 198、300、322。哈希表刷 1、128、242。刷完之后再按主题分类重刷一遍比“每天随机刷三题”更容易形成长期记忆。10. 模式识别的关键信号对照表面试或笔试时快速判断用哪种模式其实有一套“信号词”可以依赖。整理成一张表方便查阅题设关键词 / 特征优先考虑模式连续子串/子数组 最大/最小长度滑动窗口有序数组 两数/多数和双指针链表有环 / 找中点 / 删除倒数第 N 个快慢双指针数据范围极大答案存在上下界二分答案子数组和 / 区间和 存在负数前缀和 哈希所有排列/组合/子集/路径回溯岛屿/连通块/可达性DFS最短路径 / 最小步数 / 层序遍历BFS最大值 / 最小次数 / 方案数动态规划只需输出 True/False 且可跳跃贪心这个对照表用在 LeetCode 刷题里的效果拿到题先圈题干关键词再对照上表缩小范围最后才写代码。整个过程建议控制在 1 到 2 分钟内剩下时间交给实现和调试。11. 刷题避免踩坑的实践建议刷 LeetCode 只记模板而不主动复盘效果会快速衰减。几个实际使用中比较有效的习惯先写暴力解再优化。很多中等题给不了最优解但暴力解先能过小数据可以帮你确认题意理解得是否正确也方便后面验证优化结果的正确性。直接上手写滑动窗口或 DP很容易出现题目理解偏差导致全盘返工。做题必须落实“脱离题解重写”环节。看题解时感觉自己懂了合上答案自己写却卡在索引边界这是新手最常见的幻觉。正确做法是看完题解把代码合上独立重写一遍并跑题目的测试用例。能过才算真正掌握这个模板。建立“同一模式题组”的错题复盘。不要按题号顺序刷而是按模式分组。比如今天只刷滑动窗口明天只刷回溯。这样可以在短时间内反复使用同一套模板代码加深肌肉记忆。输入材料里也提到了 LeetCode 热门 100 题、周赛、剑指 Offer 等资源。周赛适合在模式熟练以后去验证而不是在刷题初期就去受挫。模板不是万能钥匙。很多困难题是多种模式的组合比如“前缀和 哈希 双指针”“二分 贪心 check”“前缀树 回溯”。如果已经确定了单一模式却越写越乱很可能题目需要复合两种模式此时应该回退到最根本的暴力枚举理清每一步状态再套高级结构。代码规范也值得注意。面试中常会要求手写代码建议从最初就养成习惯变量名用 camelCase不使用 a、b、c 之类的无意义名数组边界的和在循环入口处写注释说明避免改逻辑时出错。模板只是起点最终能在面试场上写出来的代码才是真正属于你的代码。这套 8 种解题模式框架比较适合作为第一轮系统刷题的骨架。先把上面 20 多道基础题吃透再逐步扩展到周赛和困难题会比盲目刷题更稳。建议把这篇文章收藏起来每次看到“啊这题我不会”时回来对照一次模式表很快就能定位问题在哪个环节。