大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
14 种经典刷题模式
这一课你将能做到
- 拿到一道没见过的算法题,30 秒内根据题面信号判断它属于 14 种模式中的哪一种
- 默写出每种模式的 Java 代码骨架,只往「条件判断」的位置填业务逻辑
- 给自己排出一份按模式分组的刷题顺序,而不是在题库里随机乱刷
为什么要学
很多初学者刷 LeetCode 的方式是:题号从 1 往后刷,每题从头想,想不出来看题解,一周后再做还是不会。问题不在笨,而在没有「归类」这一层。
算法题上千道,但反复考的代码结构只有十几种。NeetCode、Grokking 等题单本质上都在按模式组织:连续子串考窗口、有序配对考双指针、第 K 大考堆。先把 14 个模板写熟,做新题就是「识别 + 填空」而非「发明算法」。
先建立图像
把 14 种模式按数据结构分四堆记:数组/字符串(窗口、双指针、区间合并、循环排序、二分);链表(快慢、原地反转);树/图(BFS、DFS 回溯);特殊结构(双堆、子集、TopK、K 路归并、DP)。
口诀:连续窗口、有序双指、环中点快慢、区间先排序、归位靠循环、链表三指针、层序 BFS、穷举 DFS、中位双堆、方案子集、K 大堆、多路堆、有序二分、最值 DP。
跟着做
步骤 1:读题时先找「信号词」
别急着想解法,先在题面圈关键词:「连续 + 最长/最短」→ 窗口;「有序 + 配对/去重」→ 双指针;「环、中点、倒数 K」→ 快慢;「区间、重叠」→ 区间合并;「1..n、缺失/重复」→ 循环排序;「层序、最少步数」→ BFS;「所有排列/组合/子集」→ DFS;「第 K 大、前 K 高频、数据流」→ 堆;「K 个有序序列」→ K 路归并;「最值/计数 + 选择影响后续」→ DP。
步骤 2:完整走一遍——无重复字符的最长子串(LeetCode 3)
题面:给定字符串 s,找出不含重复字符的最长子串长度。信号词是「连续子串 + 最长」,命中滑动窗口。
import java.util.*;
public class Solution {
public int lengthOfLongestSubstring(String s) {
Map<Character, Integer> window = new HashMap<>();
int left = 0;
int answer = 0;
for (int right = 0; right < s.length(); right++) {
char c = s.charAt(right);
window.merge(c, 1, Integer::sum); // 右侧字符进窗口
while (window.get(c) > 1) { // 窗口不合法:有重复
char d = s.charAt(left);
window.merge(d, -1, Integer::sum);
window.remove(d, 0);
left++; // 左侧收缩,直到重新合法
}
answer = Math.max(answer, right - left + 1);
}
return answer;
}
}
全过程就是模板里的三件事:右指针扩张 → 不合法就左指针收缩 → 每轮更新答案。窗口的左右指针都只往右走,所以整体是 O(n)。
步骤 3:同一模板解第二道——长度最小的子数组(LeetCode 209)
题面:找出和 ≥ target 的最短连续子数组长度,只换条件:
public int minSubArrayLen(int target, int[] nums) {
int left = 0, sum = 0, answer = Integer.MAX_VALUE;
for (int right = 0; right < nums.length; right++) {
sum += nums[right];
while (sum >= target) { // 条件换成「和达标」
answer = Math.min(answer, right - left + 1);
sum -= nums[left++];
}
}
return answer == Integer.MAX_VALUE ? 0 : answer;
}
这就是模式的威力:两题表面不同,代码结构一模一样。
步骤 4:按模式分组刷题(建议 4 周节奏)
第 1 周:滑动窗口 + 双指针 + 二分;第 2 周:链表两个模式 + 区间合并 + 循环排序;第 3 周:BFS + DFS/子集;第 4 周:堆(双堆/TopK/K 路归并)+ DP 入门。每模式先背模板,再做 3 道易题验证、2 道中题变形。宁可一道题隔周重做三遍,不要三题各做一遍。
讲清楚要点
下面是 14 种模式目录,每种给「识别特征 + 代表题 + Java 模板」,建议手抄一遍。
1. 滑动窗口 Sliding Window
- 识别特征:连续子数组/子串;求最长、最短或满足条件的窗口数;窗口两端只向右移动
- 代表题:3 无重复最长子串、209 最短子数组、76 最小覆盖子串
int left = 0;
Map<Character, Integer> win = new HashMap<>();
for (int right = 0; right < s.length(); right++) {
win.merge(s.charAt(right), 1, Integer::sum);
while (/* 窗口不合法 */) {
win.merge(s.charAt(left), -1, Integer::sum);
win.remove(s.charAt(left), 0);
left++;
}
// 在此更新答案
}
2. 双指针 Two Pointers
- 识别特征:有序(或排序后处理)数组;两数配对、三数之和、去重、原地交换、盛水
- 代表题:167 两数之和 II、15 三数之和、11 盛最多水
int left = 0, right = nums.length - 1;
while (left < right) {
int sum = nums[left] + nums[right];
if (sum == target) { /* 记录答案 */ }
else if (sum < target) left++;
else right--;
}
3. 快慢指针 Fast & Slow Pointers
- 识别特征:链表判环、环的入口、找中点、倒数第 K 个;或数字不断变换会不会循环(快乐数)
- 代表题:141 环形链表、142 环的入口、876 链表中点
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;
4. 区间合并 Merge Intervals
- 识别特征:给出若干
[start, end],问合并后的区间、是否重叠、最少几个会议室;第一步永远是按起点排序 - 代表题:56 合并区间、57 插入区间、435 无重叠区间
Arrays.sort(intervals, (a, b) -> a[0] - b[0]);
List<int[]> merged = new ArrayList<>();
for (int[] cur : intervals) {
int[] last = merged.isEmpty() ? null : merged.get(merged.size() - 1);
if (last == null || last[1] < cur[0]) merged.add(cur);
else last[1] = Math.max(last[1], cur[1]);
}
5. 循环排序 Cyclic Sort
- 识别特征:数组元素取值在
1..n或0..n,找缺失数字/重复数字,且要求 O(n) 时间、O(1) 空间,不允许调用排序 - 代表题:268 丢失数字、448 消失的数字、41 缺失的第一个正数
int i = 0;
while (i < nums.length) {
int j = nums[i] - 1; // nums[i] 本应待在的下标
if (j >= 0 && j < nums.length && nums[i] != nums[j]) {
int t = nums[i]; nums[i] = nums[j]; nums[j] = t;
} else i++;
}
6. 原地反转链表 In-place Reversal
- 识别特征:要求不开新数组/新链表,翻转整条链表、翻转一段、每 K 个一组翻转
- 代表题:206 反转链表、92 反转链表 II、25 K 个一组翻转
ListNode prev = null, cur = head;
while (cur != null) {
ListNode next = cur.next; // 先存后继,否则断链
cur.next = prev;
prev = cur;
cur = next;
}
return prev;
7. 广度优先 BFS
- 识别特征:树的层序遍历(每层单独处理);图中边权相同时的最短路径、最少步数;网格扩散
- 代表题:102 层序遍历、127 单词接龙、994 腐烂橘子
Queue<TreeNode> q = new ArrayDeque<>();
q.offer(root);
while (!q.isEmpty()) {
int size = q.size(); // 先固定本层节点数
for (int i = 0; i < size; i++) {
TreeNode node = q.poll();
if (node.left != null) q.offer(node.left);
if (node.right != null) q.offer(node.right);
}
}
8. 深度优先 / 回溯 DFS & Backtracking
- 识别特征:要列出所有排列、组合、路径、棋盘方案;剪枝后的穷举
- 代表题:46 全排列、39 组合总和、79 单词搜索、200 岛屿数量
void backtrack(int[] nums, int start, List<Integer> path, List<List<Integer>> res) {
res.add(new ArrayList<>(path));
for (int i = start; i < nums.length; i++) {
path.add(nums[i]); // 选择
backtrack(nums, i + 1, path, res);
path.remove(path.size() - 1); // 撤销选择
}
}
9. 双堆 Two Heaps
- 识别特征:数据流持续进入,随时要中位数、前半段最大值、后半段最小值;需要 O(log n) 插入、O(1) 取中位
- 代表题:295 数据流中位数、480 滑动窗口中位数、502 IPO
Queue<Integer> small = new PriorityQueue<>(Comparator.reverseOrder()); // 前半:大顶堆
Queue<Integer> large = new PriorityQueue<>(); // 后半:小顶堆
public void addNum(int num) {
small.offer(num);
large.offer(small.poll());
if (large.size() > small.size()) small.offer(large.poll());
}
public double findMedian() {
return small.size() > large.size()
? small.peek()
: (small.peek() + large.peek()) / 2.0;
}
10. 子集模式 Subsets
- 识别特征:明确问「所有子集」;元素可能重复需要去重;括号生成这类每步二选一的枚举。回溯的一个高频特化,也常用迭代法
- 代表题:78 子集、90 子集 II、22 括号生成
List<List<Integer>> res = new ArrayList<>();
res.add(new ArrayList<>());
for (int num : nums) { // 每读一个新数
int n = res.size();
for (int i = 0; i < n; i++) { // 复制现有子集并追加
List<Integer> next = new ArrayList<>(res.get(i));
next.add(num);
res.add(next);
}
}
11. 二分查找 Binary Search
- 识别特征:数组有序;或答案本身单调(最小可行值、最大不可行值,即「答案二分」);旋转数组找分段点
- 代表题:704 二分查找、33 搜索旋转排序数组、875 爱吃香蕉的珂珂
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2; // 防止 (l+r) 溢出
if (nums[mid] == target) return mid;
if (nums[mid] < target) left = mid + 1;
else right = mid - 1;
}
return -1;
// 答案二分:while(left < right),可行时 right = mid
12. Top K 元素
- 识别特征:第 K 大/小、前 K 个高频元素、最接近的 K 个点;数据是流式的无法一次全排序。用大小为 K 的堆,复杂度 O(n log k)
- 代表题:215 第 K 大元素、347 前 K 高频、973 最接近原点 K 个点
Queue<Integer> heap = new PriorityQueue<>(); // 留最大的 K 个 → 小顶堆
for (int n : nums) {
heap.offer(n);
if (heap.size() > k) heap.poll();
}
13. K 路归并 K-way Merge
- 识别特征:K 个已经有序的数组或链表要合并;每一步只需从 K 个队首中取最小;小顶堆把比较从 O(K) 降到 O(log K)
- 代表题:23 合并 K 个升序链表、378 有序矩阵第 K 小、632 最小区间
Queue<ListNode> heap = new PriorityQueue<>(Comparator.comparingInt(a -> a.val));
for (ListNode node : lists) if (node != null) heap.offer(node);
ListNode dummy = new ListNode(), tail = dummy;
while (!heap.isEmpty()) {
ListNode node = heap.poll();
tail.next = node;
tail = node;
if (node.next != null) heap.offer(node.next);
}
14. 动态规划 Dynamic Programming
- 识别特征:求最值(最大、最小、最长)、计数(多少种走法)、可行性(能不能);当前选择影响后续,且子问题反复出现。先定义
dp[i]的含义,再写转移方程,最后给 base case - 代表题:70 爬楼梯、198 打家劫舍、322 零钱兑换、300 最长递增子序列
int[] dp = new int[n + 1];
dp[0] = 1; // base case
for (int i = 1; i <= n; i++) {
dp[i] = dp[i - 1] /* + 其它前置状态 */;
}
// 只依赖前 1~2 个状态时,用滚动变量把空间降到 O(1)
贯穿性要点:模板只是骨架,填错条件比不会模板更致命;数组题注意空数组、单元素等边界;链表题手绘指针指向,能解决 90% 的混乱。
你可能会懵的地方
-
14 种模式会不会有重叠?比如子集和 DFS 算哪个? 会重叠,模式不是互斥分类,而是「最常出现的代码形状」。子集本质是回溯特化,因考得频繁、模板固定才单列。一题归两类时,选你写得更熟的那个。
-
模板背下来了,做题还是不知道 while 里条件怎么写怎么办? 把「窗口合法」翻译成一句可判定的话。比如「无重复」= 每个字符计数
≤ 1;「覆盖子串」= 需要的字符计数都≥ 要求值。先写这句中文,再翻译成布尔表达式。 -
中等题经常是两个模式组合(比如滑动窗口 + 单调队列),怎么练?LeetCode 要不要全刷? 先保证单模式题看到就写,再碰组合题:入口模式决定主结构,第二个模式只优化其中一步。不必全刷,按 NeetCode 150 或代码随想录刷到每模式覆盖易/中两档即可,面试官也期待你用成熟结构搭骨架,边写边讲指针在干什么就行。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
| 随机按题号刷,不做归类 | 每题都在重新发明,重复率高 | 按模式分组,先模板后变形 |
二分写 mid = (left + right) / 2 | 大数组时整型溢出 | left + (right - left) / 2 |
反转链表不保存 cur.next | 指针一断,后半截链表丢失 | 先存 next,再改指向 |
| 回溯忘记撤销选择 | path 越带越长,答案全错 | 递归返回后立即 remove 最后一个元素 |
TopK 上来就全量 sort | 流式数据无法排序,也更慢 | 维护大小为 K 的堆,O(n log k) |
| DP 不先定义 dp 含义直接写转移 | 写到一半下标错乱 | 先写「dp[i] 表示……」再写方程 |
卡点急救
- 识别不出模式:对照信号词表;仍判断不了就先想暴力解法,其中最浪费的一步往往就是模式入口。
- 模板写出但答案不对:别盯代码,拿长度 1~2 的用例手画指针/窗口每步变化,错误多在更新时机和 while 条件。
- 超时(TLE):先确认复杂度是否达标,常见情况是窗口内线性查找(换 Map)、BFS 重复入队(加 visited)、DFS 重复子问题(加记忆化或改 DP)。
面试常问
-
给一个数组和 K,求第 K 大元素,你会怎么做? 三种答法体现层次:排序 O(n log n);小顶堆维护 K 个元素 O(n log k);快速选择平均 O(n)。先答堆(最稳、适合流式),再补充快速选择最坏 O(n²) 的风险。
-
怎么判断链表有没有环?为什么快慢指针一定能相遇? 慢指针走 1 步、快指针走 2 步。有环时两针相对速度为每轮 1 步,快针必然某轮追上而不会跳过;无环则快针先到 null。O(n) 时间 O(1) 空间。
-
动态规划和回溯/贪心有什么区别? 回溯穷举所有方案不复用;DP 把子问题结果存下来复用,处理有重叠子问题的最值;贪心每步只取当前最优且不回头,需证明局部最优能推出全局最优。
自测三问
- 滑动窗口为什么是 O(n)?你能讲清左右指针各只遍历一次吗?
- 不看资料,默写滑动窗口、BFS、二分、反转链表四个模板,能一次写对吗?
- 给三道陌生中等题,你能在 30 秒内说出各自的模式吗?
小结一句话
算法题千变万化,不过是 14 种代码形状的识别与填空——按模式背模板、用题目验模板,刷题效率会数倍提升。
延伸
- NeetCode 题单(按模式组织,强烈推荐)
- 代码随想录(中文刷题顺序与讲解)
- LeetCode 中文站
- 下一课:系统设计入门——容量估算与 4S 分析法,拆解短链、限流器和 feed 流。