Skip to content

LeetCode 分类题解 ​

刷题最大的浪费是「做完一道只会一道」。这一页按套路分类 —— 看到题先问「它属于哪一类」,再套模板。 每类给:识别特征 → 模板 → 5 道代表题(题号 + 一句话思路)。

用法 ​

  1. 先看「识别特征」,确认这类题长什么样。
  2. 背模板(只有几行),然后去做代表题。
  3. 做不出来不要立刻看答案,先问自己「卡在识别还是卡在实现」。

一、双指针 ​

特征:数组有序、要求「两个数/三段区间」、原地修改、去重。

java
// 对撞双指针:两数之和(有序)
int l = 0, r = n - 1;
while (l < r) {
    int sum = a[l] + a[r];
    if (sum == target) return new int[]{l, r};
    if (sum < target) l++; else r--;
}
// 对撞双指针:两数之和(有序)
int l = 0, r = n - 1;
while (l < r) {
    int sum = a[l] + a[r];
    if (sum == target) return new int[]{l, r};
    if (sum < target) l++; else r--;
}
题号题思路
167两数之和 II上面模板
15三数之和排序 + 固定一个 + 对撞,注意去重
11盛最多水的容器移动矮的那边
283移动零快慢指针,把非零往前挤
42接雨水双指针维护左右最大值

易错点:三数之和的去重是 nums[i] == nums[i-1] 而不是 nums[i] == nums[i+1]。


二、滑动窗口 ​

特征:「最长/最短的连续子串/子数组」+ 一个可维护的条件。

java
int left = 0, ans = 0;
Map<Character, Integer> win = new HashMap<>();
for (int right = 0; right < s.length(); right++) {
    // 1. 右指针进窗
    win.merge(s.charAt(right), 1, Integer::sum);
    // 2. 窗口不合法就收缩左边界
    while (/* 不合法 */) {
        win.merge(s.charAt(left), -1, Integer::sum);
        left++;
    }
    // 3. 更新答案
    ans = Math.max(ans, right - left + 1);
}
int left = 0, ans = 0;
Map<Character, Integer> win = new HashMap<>();
for (int right = 0; right < s.length(); right++) {
    // 1. 右指针进窗
    win.merge(s.charAt(right), 1, Integer::sum);
    // 2. 窗口不合法就收缩左边界
    while (/* 不合法 */) {
        win.merge(s.charAt(left), -1, Integer::sum);
        left++;
    }
    // 3. 更新答案
    ans = Math.max(ans, right - left + 1);
}
题号题思路
3无重复字符最长子串窗口内字符不重复
76最小覆盖子串窗口需覆盖 t 的所有字符,收缩条件是「已覆盖」
438找所有字母异位词固定长度窗口 + 计数数组比较
209长度最小子数组和 >= target 就收缩
239滑动窗口最大值单调队列(见「栈」一节)

易错点:先 while 收缩再更新答案 —— 顺序反了答案就偏大。


三、哈希与计数 ​

特征:查「出现过没有」「出现几次」「两数配对」。

题号题思路
1两数之和边遍历边存 target - x
49字母异位词分组排序后的串 / 26 位计数当 key
128最长连续序列只从「没有前驱」的数开始数
560和为 K 的子数组前缀和 + 哈希计数
347前 K 个高频元素计数 + 小顶堆 / 桶排序

模板(前缀和 + 哈希):

java
Map<Integer, Integer> cnt = new HashMap<>();
cnt.put(0, 1);              // 关键:空前缀
int sum = 0, ans = 0;
for (int x : nums) {
    sum += x;
    ans += cnt.getOrDefault(sum - k, 0);
    cnt.merge(sum, 1, Integer::sum);
}
Map<Integer, Integer> cnt = new HashMap<>();
cnt.put(0, 1);              // 关键:空前缀
int sum = 0, ans = 0;
for (int x : nums) {
    sum += x;
    ans += cnt.getOrDefault(sum - k, 0);
    cnt.merge(sum, 1, Integer::sum);
}

四、链表 ​

特征:改指针、找环、合并、删除倒数第 N 个。

java
// 必备:哑结点(dummy)能省掉所有头节点特判
ListNode dummy = new ListNode(0, head), prev = dummy;

// 必备:快慢指针找中点 / 判环
ListNode slow = head, fast = head;
while (fast != null && fast.next != null) { slow = slow.next; fast = fast.next.next; }
// 必备:哑结点(dummy)能省掉所有头节点特判
ListNode dummy = new ListNode(0, head), prev = dummy;

// 必备:快慢指针找中点 / 判环
ListNode slow = head, fast = head;
while (fast != null && fast.next != null) { slow = slow.next; fast = fast.next.next; }
题号题思路
206反转链表三指针(见手写题清单)
25K 个一组翻转分组 + 组内反转 + 拼接
141 / 142环形链表快慢指针相遇后,一个回头,再走相遇即入口
21合并两个有序链表哑结点 + 双指针
146LRU 缓存HashMap + 双向链表(见手写题清单)

易错点:反转后忘记把尾节点的 next 置空,导致新链表带环。


五、二叉树 ​

特征:「前中后序」「层序」「路径」「最近公共祖先」。

java
// 递归三要素:出口、当前层做什么、返回什么
int dfs(TreeNode node) {
    if (node == null) return 0;
    int left = dfs(node.left), right = dfs(node.right);
    return Math.max(left, right) + 1;   // 例:求深度
}

// 层序:队列 + 每层 size
Deque<TreeNode> q = new ArrayDeque<>();
q.offer(root);
while (!q.isEmpty()) {
    int size = q.size();
    while (size-- > 0) {
        TreeNode n = q.poll();
        if (n.left != null) q.offer(n.left);
        if (n.right != null) q.offer(n.right);
    }
}
// 递归三要素:出口、当前层做什么、返回什么
int dfs(TreeNode node) {
    if (node == null) return 0;
    int left = dfs(node.left), right = dfs(node.right);
    return Math.max(left, right) + 1;   // 例:求深度
}

// 层序:队列 + 每层 size
Deque<TreeNode> q = new ArrayDeque<>();
q.offer(root);
while (!q.isEmpty()) {
    int size = q.size();
    while (size-- > 0) {
        TreeNode n = q.poll();
        if (n.left != null) q.offer(n.left);
        if (n.right != null) q.offer(n.right);
    }
}
题号题思路
94 / 144 / 145三种遍历递归;迭代版用栈
102层序遍历上面模板
236最近公共祖先左右各找到就返回当前节点
124最大路径和后序,返回单边最大,全局答案取两边和
105前序+中序重建前序首元素切中序

易错点:124 这类题要区分「返回给父节点的值」和「更新全局答案的值」。


六、图与搜索 ​

特征:网格、连通块、最短路(无权)、拓扑排序。

java
// 网格 BFS(最短路模板)
int[][] dirs = {{1,0},{-1,0},{0,1},{0,-1}};
Deque<int[]> q = new ArrayDeque<>();
q.offer(new int[]{sr, sc});
boolean[][] seen = new boolean[m][n];
seen[sr][sc] = true;
int step = 0;
while (!q.isEmpty()) {
    for (int i = q.size(); i > 0; i--) {
        int[] cur = q.poll();
        if (cur[0] == tr && cur[1] == tc) return step;
        for (int[] d : dirs) {
            int x = cur[0] + d[0], y = cur[1] + d[1];
            if (x < 0 || y < 0 || x >= m || y >= n) continue;
            if (seen[x][y] || grid[x][y] == 1) continue;
            seen[x][y] = true;
            q.offer(new int[]{x, y});
        }
    }
    step++;
}
// 网格 BFS(最短路模板)
int[][] dirs = {{1,0},{-1,0},{0,1},{0,-1}};
Deque<int[]> q = new ArrayDeque<>();
q.offer(new int[]{sr, sc});
boolean[][] seen = new boolean[m][n];
seen[sr][sc] = true;
int step = 0;
while (!q.isEmpty()) {
    for (int i = q.size(); i > 0; i--) {
        int[] cur = q.poll();
        if (cur[0] == tr && cur[1] == tc) return step;
        for (int[] d : dirs) {
            int x = cur[0] + d[0], y = cur[1] + d[1];
            if (x < 0 || y < 0 || x >= m || y >= n) continue;
            if (seen[x][y] || grid[x][y] == 1) continue;
            seen[x][y] = true;
            q.offer(new int[]{x, y});
        }
    }
    step++;
}
题号题思路
200岛屿数量DFS/BFS 染色,数连通块
207课程表拓扑排序(入度 + 队列)
994腐烂的橘子多源 BFS,答案是层数
1091二进制矩阵最短路八方向 BFS
547省份数量并查集 / DFS

易错点:多源 BFS 的起点要一次全入队,不能一个个来。


七、回溯 ​

特征:「所有组合 / 排列 / 子集 / 分割」,且要求列出全部方案。

java
void backtrack(int start, List<Integer> path) {
    res.add(new ArrayList<>(path));       // 1. 收集(这里是无条件收集子集)
    for (int i = start; i < n; i++) {
        if (i > start && a[i] == a[i - 1]) continue;  // 2. 同层去重
        path.add(a[i]);                   // 3. 做选择
        backtrack(i + 1, path);           // 4. 递归(组合用 i+1,排列用 visited)
        path.remove(path.size() - 1);     // 5. 撤销
    }
}
void backtrack(int start, List<Integer> path) {
    res.add(new ArrayList<>(path));       // 1. 收集(这里是无条件收集子集)
    for (int i = start; i < n; i++) {
        if (i > start && a[i] == a[i - 1]) continue;  // 2. 同层去重
        path.add(a[i]);                   // 3. 做选择
        backtrack(i + 1, path);           // 4. 递归(组合用 i+1,排列用 visited)
        path.remove(path.size() - 1);     // 5. 撤销
    }
}
题号题思路
78子集上面模板(无条件收集)
46全排列用 visited[] 而不是 start
39 / 40组合总和可重复传 i,不可重复传 i+1
51N 皇后列/主对角/副对角三个集合剪枝
131分割回文串切分点做选择 + 回文预判

易错点:去重必须先排序,且判重是「同一层跳过」而不是「整棵树跳过」。


八、动态规划 ​

特征:求「最值 / 方案数」,且当前状态只依赖更小的子问题。

java
// 四步:定义状态 -> 转移 -> 初始化 -> 遍历顺序
int[] dp = new int[n + 1];
dp[0] = 1;                       // 初始化(空串/空背包通常为 1)
for (int i = 1; i <= n; i++) {
    for (int j = 0; j < i; j++) {
        if (/* 可转移 */) dp[i] += dp[j];
    }
}
// 四步:定义状态 -> 转移 -> 初始化 -> 遍历顺序
int[] dp = new int[n + 1];
dp[0] = 1;                       // 初始化(空串/空背包通常为 1)
for (int i = 1; i <= n; i++) {
    for (int j = 0; j < i; j++) {
        if (/* 可转移 */) dp[i] += dp[j];
    }
}
题号题状态定义
70爬楼梯dp[i] 到第 i 阶的方案数
198打家劫舍dp[i] 前 i 间最高金额
322零钱兑换dp[amount] 凑出该金额的最少硬币
300最长递增子序列dp[i] 以 i 结尾的 LIS 长度(可优化到 O(n log n))
72编辑距离dp[i][j] 前 i 与前 j 的编辑距离

易错点:遍历顺序 —— 01 背包容量倒序,完全背包正序;二维 DP 先想清楚行列代表什么。


九、栈与单调栈 ​

特征:「下一个更大/更小」「括号匹配」「表达式求值」。

java
// 下一个更大元素(单调递减栈)
int[] nextGreater(int[] nums) {
    int n = nums.length;
    int[] res = new int[n];
    Arrays.fill(res, -1);
    Deque<Integer> st = new ArrayDeque<>();  // 存下标
    for (int i = 0; i < n; i++) {
        while (!st.isEmpty() && nums[i] > nums[st.peek()]) res[st.pop()] = nums[i];
        st.push(i);
    }
    return res;
}
// 下一个更大元素(单调递减栈)
int[] nextGreater(int[] nums) {
    int n = nums.length;
    int[] res = new int[n];
    Arrays.fill(res, -1);
    Deque<Integer> st = new ArrayDeque<>();  // 存下标
    for (int i = 0; i < n; i++) {
        while (!st.isEmpty() && nums[i] > nums[st.peek()]) res[st.pop()] = nums[i];
        st.push(i);
    }
    return res;
}
题号题思路
20有效括号左括号入栈,右括号比对
155最小栈辅助栈存当前最小值
84柱状图最大矩形单调递增栈 + 哨兵
739每日温度上面模板
239滑动窗口最大值单调递减队列

易错点:栈里存下标而不是值,否则算不出宽度。


十、位运算与数学 ​

特征:「只出现一次」「不用额外空间」「2 的幂」。

题号题技巧
136只出现一次的数字全部异或,成对消掉
137只出现一次 II按位统计 mod 3
260只出现两个一次异或后取最低位的 1 分组
1911 的个数n &= n - 1 每次消掉最低位的 1
2312 的幂n > 0 && (n & (n-1)) == 0

刷题节奏建议 ​

阶段做法
第 1 遍按分类刷,每类 5 题,只求能套模板
第 2 遍打乱顺序刷,练习「识别属于哪类」
第 3 遍只刷错题,限时 20 分钟/题

别做的事:一天刷 20 道新题然后全忘;背题解但讲不出为什么。

配套:题型之外的「手写实现」见 手写题清单,纯数据结构与复杂度见 常用数据结构、复杂度分析。