# 高频面试题分类汇总 (总计 70+ 题)
## 一、 链表 (LinkedList) - *重中之重*
**基础操作**
- [206. 反转链表](https://leetcode-cn.com/problems/reverse-linked-list/) (Easy) - *必背*
> 给你单链表的头节点 `head` ,请你反转链表,并返回反转后的链表。
- [92. 反转链表 II](https://leetcode-cn.com/problems/reverse-linked-list-ii/) (Medium) - *区间反转*
> 给你单链表的头节点 `head` 和两个整数 `left` 和 `right` ,请你反转从位置 `left` 到位置 `right` 的链表节点,返回反转后的链表。
- [25. K 个一组翻转链表](https://leetcode-cn.com/problems/reverse-nodes-in-k-group/) (Hard) - *面试常客*
> 给你一个链表,每 `k` 个节点一组进行翻转,请你返回翻转后的链表。
- [21. 合并两个有序链表](https://leetcode-cn.com/problems/merge-two-sorted-lists/) (Easy)
> 将两个升序链表合并为一个新的升序链表并返回。
- [23. 合并K个排序链表](https://leetcode-cn.com/problems/merge-k-sorted-lists/) (Hard) - *堆/归并*
> 给你一个链表数组,每个链表都已经按升序排列。请你将所有链表合并到一个升序链表中。
- [148. 排序链表](https://leetcode-cn.com/problems/sort-list/) (Medium) - *归并排序*
> 给你链表的头结点 `head` ,请将其按升序排列并返回排序后的链表(要求 $O(n \log n)$ 时间复杂度和 $O(1)$ 空间复杂度)。
- [补充题1. 排序奇升偶降链表](https://leetcode-cn.com/problems/sort-list/) (Medium)
> 给定一个奇数位升序,偶数位降序的链表,将其排序为升序。 (思路:拆分、反转偶数链表、合并)
**双指针技巧**
- [141. 环形链表](https://leetcode-cn.com/problems/linked-list-cycle/) (Easy) - *判圈*
> 给你一个链表的头节点 `head` ,判断链表中是否有环。
- [142. 环形链表 II](https://leetcode-cn.com/problems/linked-list-cycle-ii/) (Medium) - *找入口*
> 给定一个链表,返回链表开始入环的第一个节点。如果链表无环,则返回 `null`。
- [160. 相交链表](https://leetcode-cn.com/problems/intersection-of-two-linked-lists/) (Easy)
> 给你两个单链表的头节点 `headA` 和 `headB` ,请你找出并返回两个单链表相交的起始节点。
- [19. 删除链表的倒数第N个节点](https://leetcode-cn.com/problems/remove-nth-node-from-end-of-list/) (Medium)
> 给你一个链表,删除链表的倒数第 `n` 个结点,并且返回链表的头结点。
- [剑指 Offer 22. 链表中倒数第k个节点](https://leetcode-cn.com/problems/lian-biao-zhong-dao-shu-di-kge-jie-dian-lcof/) (Easy)
> 输入一个链表,输出该链表中倒数第 `k` 个节点。
**综合/技巧**
- [143. 重排链表](https://leetcode-cn.com/problems/reorder-list/) (Medium) - *中点+反转+合并*
> 给定一个单链表 $L_0 \to L_1 \to \dots \to L_{n-1} \to L_n$ ,将其重新排列后变为: $L_0 \to L_n \to L_1 \to L_{n-1} \to L_2 \to L_{n-2} \to \dots$
- [2. 两数相加](https://leetcode-cn.com/problems/add-two-numbers/) (Medium)
> 给你两个非空的链表,表示两个非负的整数。它们每位数字都是按照逆序的方式存储的。请你将两个数相加。
- [146. LRU缓存机制](https://leetcode-cn.com/problems/lru-cache/) (Medium) - *双向链表+哈希*
> 设计并实现一个满足 LRU (最近最少使用) 缓存约束的数据结构。
- [82. 删除排序链表中的重复元素 II](https://leetcode-cn.com/problems/remove-duplicates-from-sorted-list-ii/) (Medium)
> 给定一个已排序的链表的头 `head` ,删除所有含有重复数字的节点,只保留原始链表中未重复出现的数字。
---
## 二、 二叉树 (Binary Tree) - *必考专题*
**遍历 (BFS/DFS)**
- [102. 二叉树的层序遍历](https://leetcode-cn.com/problems/binary-tree-level-order-traversal/) (Medium) - *BFS模板*
> 给你二叉树的根节点 `root` ,返回其节点值的层序遍历(即逐层地,从左到右访问所有节点)。
- [103. 二叉树的锯齿形层次遍历](https://leetcode-cn.com/problems/binary-tree-zigzag-level-order-traversal/) (Medium)
> 给你二叉树的根节点 `root` ,返回其节点值的锯齿形层序遍历(先从左往右,下一层再从右往左,以此类推,层与层之间交替进行)。
- [199. 二叉树的右视图](https://leetcode-cn.com/problems/binary-tree-right-side-view/) (Medium)
> 给定一个二叉树的根节点 `root`,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。
- [662. 二叉树最大宽度](https://leetcode-cn.com/problems/maximum-width-of-binary-tree/) (Medium)
> 给定一个二叉树,编写一个函数来获取这个树的最大宽度。每一层的宽度被定义为两个端点之间的长度。
- [94. 二叉树的中序遍历](https://leetcode-cn.com/problems/binary-tree-inorder-traversal/) (Easy)
> 给你二叉树的根节点 `root` ,返回它节点值的中序遍历。
**路径与属性 (分治思维)**
- [101. 对称二叉树](https://leetcode-cn.com/problems/symmetric-tree/) (Easy)
> 给你一个二叉树的根节点 `root` ,检查它是否轴对称。
- [105. 从前序与中序遍历序列构造二叉树](https://leetcode-cn.com/problems/construct-binary-tree-from-preorder-and-inorder-traversal/) (Medium)
> 给定两个整数数组 `preorder` 和 `inorder` ,请构造二叉树并返回其根节点。
- [236. 二叉树的最近公共祖先](https://leetcode-cn.com/problems/lowest-common-ancestor-of-a-binary-tree/) (Medium) - *必考*
> 给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
- [124. 二叉树中的最大路径和](https://leetcode-cn.com/problems/binary-tree-maximum-path-sum/) (Hard)
> 二叉树中的最大路径和是指路径上节点值的最大和。路径可以是任何节点作为起点和终点。
- [112. 路径总和](https://leetcode-cn.com/problems/path-sum/) (Easy)
> 给你二叉树的根节点 `root` 和一个表示目标和的整数 `targetSum` 。判断该树中是否存在根节点到叶子节点的路径且和等于目标和。
- [113. 路径总和 II](https://leetcode-cn.com/problems/path-sum-ii/) (Medium)
> 给你二叉树的根节点 `root` 和一个整数 `targetSum` ,找出所有从根节点到叶子节点路径总和等于给定目标和的路径。
- [129. 求根到叶子节点数字之和](https://leetcode-cn.com/problems/sum-root-to-leaf-numbers/) (Medium)
> 计算从根节点到叶子节点生成的所有数字之和。每条路径代表一个数字(如 $1 \to 2 \to 3$ 代表 $123$)。
- [剑指 Offer 26. 树的子结构](https://leetcode-cn.com/problems/shu-de-zi-jie-gou-lcof/) (Medium)
> 输入两棵二叉树A和B,判断B是不是A的子结构。
**二叉搜索树 (BST)**
- [98. 验证二叉搜索树](https://leetcode-cn.com/problems/validate-binary-search-tree/) (Medium)
> 给你一个二叉树的根节点 `root` ,判断其是否是一个有效的二叉搜索树。
---
## 三、 数组与双指针 (Array & Two Pointers)
**基础双指针 (Two Pointers Basics)**
- [167. 两数之和 II](https://leetcode-cn.com/problems/two-sum-ii-input-array-is-sorted/) (Easy) - *左右指针*
> 给你一个已按 **非递减顺序排列** 的整数数组 `numbers` ,请你从数组中找出两个数满足相加之和等于目标数 `target` 。
- [15. 三数之和](https://leetcode-cn.com/problems/3sum/) (Medium) - *排序+双指针*
> 给你一个包含 `n` 个整数的数组 `nums`,判断 `nums` 中是否存在三个元素 $a, b, c$ ,使得 $a + b + c = 0$ ?找出所有和为 0 且不重复的三元组。
- [42. 接雨水](https://leetcode-cn.com/problems/trapping-rain-water/) (Hard) - *必考*
> 给定 `n` 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨后能接多少雨水。
**滑动窗口 (Sliding Window)**
- [3. 无重复字符的最长子串](https://leetcode-cn.com/problems/longest-substring-without-repeating-characters/) (Medium) - *模版题*
> 给定一个字符串 `s` ,请你找出其中不含有重复字符的 **最长子串** 的长度。
- [209. 长度最小的子数组](https://leetcode-cn.com/problems/minimum-size-subarray-sum/) (Medium)
> 给定一个含有 `n` 个正整数的数组和一个正整数 `target` 。找出该数组中满足其和 $\ge target$ 的长度最小的 **连续子数组**。
- [76. 最小覆盖子串](https://leetcode-cn.com/problems/minimum-window-substring/) (Hard)
> 给你一个字符串 `s` 、一个字符串 `t` 。返回 `s` 中包含 `t` 所有字符的最小子串。
- [239. 滑动窗口最大值](https://leetcode-cn.com/problems/sliding-window-maximum/) (Hard) - *单调队列*
> 给你一个整数数组 `nums`,有一个大小为 `k` 的滑动窗口从数组的最左侧移动到数组的最右侧。返回滑动窗口中的最大值。
**对撞双指针** *(左右向中间逼近)*
- [42. 接雨水](https://leetcode-cn.com/problems/trapping-rain-water/) (Hard) - *必考,左右指针+维护最大高度*
> (同上,描述见上文)
- [88. 合并两个有序数组](https://leetcode-cn.com/problems/merge-sorted-array/) (Easy) - *逆向双指针,从后往前填充*
> 给你两个按非降序排列的整数数组 `nums1` 和 `nums2`,请你将 `nums2` 合并到 `nums1` 中,使合并后的数组同样按非降序排列。
- [11. 盛最多水的容器](https://leetcode-cn.com/problems/container-with-most-water/) (Medium) - *移动较短的一边*
> 给定一个长度为 `n` 的整数数组 `height` 。找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。返回最大水量。
**快慢指针** *(同向不同速)*
- [26. 删除有序数组中的重复项](https://leetcode-cn.com/problems/remove-duplicates-from-sorted-array/) (Easy) - *slow记录有效位置*
> 给你一个非降序排列的数组 `nums` ,请你 **原地** 删除重复出现的元素,并在数组的每个元素只出现一次的情况下,返回删除后数组的新长度。
- [27. 移除元素](https://leetcode-cn.com/problems/remove-element/) (Easy) - *原地删除*
> 给你一个数组 `nums` 和一个值 `val`,你需要 **原地** 移除所有数值等于 `val` 的元素,并返回移除后数组的新长度。
- [283. 移动零](https://leetcode-cn.com/problems/move-zeroes/) (Easy) - *非零前移,尾部填零*
> 给定一个数组 `nums`,编写一个函数将所有 `0` 移动到数组的末尾,同时保持非零元素的相对顺序。
- [287. 寻找重复数](https://leetcode-cn.com/problems/find-the-duplicate-number/) (Medium) - *Floyd判圈法*
> 给定一个包含 `n + 1` 个整数的数组 `nums` ,其数字都在 `[1, n]` 范围内,可知至少存在一个重复的整数。找出这个重复的数(不修改数组,使用 $O(1)$ 额外空间)。
**贪心策略**
- [121. 买卖股票的最佳时机](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock/) (Easy) - *维护历史最低价*
> 给定一个数组 `prices` ,其中 `prices[i]` 表示一支给定股票第 `i` 天的价格。如果你最多只允许完成一笔交易(即买入和卖出一支股票一次),设计一个算法来计算你所能获取的最大利润。
- [122. 买卖股票的最佳时机 II](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-ii/) (Medium) - *累加所有正收益*
> 给你一个整数数组 `prices` ,其中 `prices[i]` 表示某支股票第 `i` 天的价格。在每一天,你可以决定是否购买和/或出售股票。计算你所能获得的最大利润。
### 贪心策略模板 (股票问题)
> **核心思想**:
> 1. **单次交易 (121)**:贪心地假设自己是在历史最低点买入的。遍历时持续更新“历史最低价 (`minPrice`)”,并计算“当前价格卖出能赚多少 (`price - minPrice`)”,取最大值。
> 2. **无限次交易 (122)**:贪心地收集所有“上坡”的收益。只要今天的价格比昨天高,我就假装昨天买今天卖(`prices[i] - prices[i-1]`),把所有正收益累加起来就是最大利润。
代码实现 (点击展开)
```javascript
// 121. 买卖股票的最佳时机 (只允许一次交易)
var maxProfit = function(prices) {
let profit = 0;
// 方法:我们假设第 0 天买入,然后找之后价格更高的卖出
// 实际操作:维护一个 minPrice 成本线
let minPrice = prices[0];
for (let i = 1; i < prices.length; i++) { // 从第 1 天开始看
if (prices[i] > minPrice) {
// 如果今天比成本线高,尝试卖出,看是不是能赚更多
profit = Math.max(profit, prices[i] - minPrice);
} else {
// 如果今天比成本线还低,那不如今天买入(更新成本线)
minPrice = prices[i];
}
}
return profit;
};
// 122. 买卖股票的最佳时机 II (允许无限次交易)
var maxProfitII = function(prices) {
let profit = 0;
// 方法:我们只看从昨天到今天这一段,能不能赚钱
// 实际操作:比较今天和昨天的价格
for (let i = 1; i < prices.length; i++) { // 从第 1 天开始看
if (prices[i] > prices[i - 1]) {
// 如果今天比昨天高,就赚这个差价(收集上坡)
profit += prices[i] - prices[i - 1];
}
// 如果比昨天低,就不动(不把亏损算进去),也无需更新 minPrice,因为我们可以每天都操作
}
return profit;
};
```
**矩阵模拟 (边界控制)**
- [54. 螺旋矩阵](https://leetcode-cn.com/problems/spiral-matrix/) (Medium) - *四边界收缩*
> 给你一个 `m` 行 `n` 列的矩阵 `matrix` ,请按照顺时针螺旋顺序,返回矩阵中的所有元素。
- [48. 旋转图像](https://leetcode-cn.com/problems/rotate-image/) (Medium) - *先转置后翻转 / 四角轮换*
> 给定一个 $n \times n$ 的二维矩阵 `matrix` 表示一个图像。请你将图像顺时针旋转 90 度(原地旋转)。
- [31. 下一个排列](https://leetcode-cn.com/problems/next-permutation/) (Medium) - *规律模拟:找降序起点*
> 给你一个整数数组 `nums` ,找出 `nums` 的下一个字典序更大的排列。
### 模拟/数学策略模板
> **核心思想**:
> 1. **矩阵操作**:通常不需要复杂算法,而是需要**精确控制边界 (Top/Bottom/Left/Right)** 或者利用**几何数学变换 (转置+镜像)**。
> 2. **排列问题**:观察数字规律。涉及顺序变换时,往往从**倒序遍历**找那个“破坏单调性”的点开始。
代码实现 (点击展开)
```javascript
// 54. 螺旋矩阵 (四边界收缩法)
var spiralOrder = function(matrix) {
if (!matrix.length) return [];
// 1. 定义四个边界
let top = 0, bottom = matrix.length - 1;
let left = 0, right = matrix[0].length - 1;
const res = [];
// 2. 循环直到边界交错
while (true) {
// 向右移动 (top行)
for (let i = left; i <= right; i++) res.push(matrix[top][i]);
if (++top > bottom) break; // 上边界下移,检查是否越界
// 向下移动 (right列)
for (let i = top; i <= bottom; i++) res.push(matrix[i][right]);
if (--right < left) break; // 右边界左移
// 向左移动 (bottom行)
for (let i = right; i >= left; i--) res.push(matrix[bottom][i]);
if (--bottom < top) break; // 下边界上移
// 向上移动 (left列)
for (let i = bottom; i >= top; i--) res.push(matrix[i][left]);
if (++left > right) break; // 左边界右移
}
return res;
};
// 48. 旋转图像 (数学变换法)
// 顺时针转90度 = 先水平上下翻转 + 再对角线翻转 (写法很多,这种最好记)
// 或者:先对角线转置 + 再左右翻转
var rotate = function(matrix) {
const n = matrix.length;
// 1. 先水平上下翻转 (Top <-> Bottom)
// 1 2 3 7 8 9
// 4 5 6 => 4 5 6
// 7 8 9 1 2 3
let top = 0, bottom = n - 1;
while (top < bottom) {
[matrix[top], matrix[bottom]] = [matrix[bottom], matrix[top]];
top++;
bottom--;
}
// 2. 再对角线翻转 (Swap matrix[i][j] with matrix[j][i])
// 7 8 9 7 4 1
// 4 5 6 => 8 5 2
// 1 2 3 9 6 3
for (let i = 0; i < n; i++) {
for (let j = i + 1; j < n; j++) { // 注意 j 从 i+1 开始,只遍历上三角
[matrix[i][j], matrix[j][i]] = [matrix[j][i], matrix[i][j]];
}
}
};
// 31. 下一个排列 (三步走)
var nextPermutation = function(nums) {
let i = nums.length - 2;
// 1. 从后向前找第一个【升序对】 (i, i+1),即 nums[i] < nums[i+1]
// 此时 [i+1, end] 肯定是降序的
while (i >= 0 && nums[i] >= nums[i + 1]) {
i--;
}
if (i >= 0) {
// 2. 从后向前找第一个比 nums[i] 大的数 nums[j]
let j = nums.length - 1;
while (j >= 0 && nums[j] <= nums[i]) {
j--;
}
// 交换它们,让大一点点的数排到前面
[nums[i], nums[j]] = [nums[j], nums[i]];
}
// 3. 将 [i+1, end] 这部分降序的数组反转成升序,使其变小
let left = i + 1;
let right = nums.length - 1;
while (left < right) {
[nums[left], nums[right]] = [nums[right], nums[left]];
left++;
right--;
}
};
```
**区间处理 & 原地哈希**
- [56. 合并区间](https://leetcode-cn.com/problems/merge-intervals/) (Medium) - *排序后贪心合并*
> 以数组 `intervals` 表示若干个区间的集合,合并所有重叠的区间,并返回一个不重叠的区间数组。
- [41. 缺失的第一个正数](https://leetcode-cn.com/problems/first-missing-positive/) (Hard) - *原地哈希:nums[i]放到i-1位置*
> 给你一个未排序的整数数组 `nums` ,请你找出其中没有出现的最小的正整数(要求 $O(n)$ 时间复杂度和 $O(1)$ 空间复杂度)。
---
## 四、 动态规划 (DP)
> **DP四要素**:① 状态定义 → ② 转移方程 → ③ 初始化 → ④ 返回值
---
### 模板一:线性DP (单序列)
> **状态**:`dp[i]` = 以 `nums[i]` 结尾/到达位置 `i` 的最优解
> **特点**:当前状态只依赖前面的状态
```javascript
// 通用模板
const dp = new Array(n).fill(初始值); // 1. 状态定义:dp[i]
dp[0] = 边界值; // 2. 初始化
for (let i = 1; i < n; i++) {
dp[i] = 状态转移(dp[i-1], dp[i-2], ...); // 3. 转移方程
}
return dp[n-1]; // 4. 返回值 (或 Math.max(...dp))
```
| 题目 | 题干 | 状态定义 | 转移方程 |
| :-------------------------------------------------------------------------------------- | :---------------------------------------------------- | :-------------------------- | :--------------------------------------------------------- |
| [70. 爬楼梯](https://leetcode-cn.com/problems/climbing-stairs/) | 需要 n 阶到达楼顶,每次可爬 1 或 2 阶,求方法数 | `dp[i]` = 到第i阶的方法数 | `dp[i] = dp[i-1] + dp[i-2]` |
| [53. 最大子数组和](https://leetcode-cn.com/problems/maximum-subarray/) | 找出具有最大和的 **连续子数组**,返回其最大和 | `dp[i]` = 以i结尾的最大和 | `dp[i] = max(dp[i-1] + nums[i], nums[i])` |
| [300. 最长上升子序列](https://leetcode-cn.com/problems/longest-increasing-subsequence/) | 找到其中最长严格递增子序列的长度 | `dp[i]` = 以i结尾的LIS长度 | `dp[i] = max(dp[j] + 1)` 其中 `j < i && nums[j] < nums[i]` |
| [139. 单词拆分](https://leetcode-cn.com/problems/word-break/) | 判定字符串 `s` 是否可以由 `wordDict` 中的单词拼接而成 | `dp[i]` = 前i个字符能否拆分 | `dp[i] = dp[j] && s[j:i] in dict` |
代码实现 (点击展开)
```javascript
// 70. 爬楼梯
var climbStairs = function(n) {
if (n <= 1) return 1;
const dp = new Array(n + 1).fill(0); // 状态:dp[i] 到第i阶的方法数
dp[0] = 1; dp[1] = 1; // 初始化
for (let i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2]; // 方程:前两阶方法数之和
}
return dp[n]; // 答案
};
// 53. 最大子数组和
var maxSubArray = function(nums) {
const n = nums.length;
const dp = new Array(n).fill(0); // 状态:以i结尾的最大子数组和
dp[0] = nums[0]; // 初始化
for (let i = 1; i < n; i++) {
// 方程:要么接在前面后面,要么自立门户
dp[i] = Math.max(dp[i - 1] + nums[i], nums[i]);
}
return Math.max(...dp); // 答案:所有结尾情况中的最大值
};
// 300. 最长上升子序列
var lengthOfLIS = function(nums) {
const n = nums.length;
if (n === 0) return 0;
const dp = new Array(n).fill(1); // 状态:以i结尾的LIS长度,初始化为1
for (let i = 1; i < n; i++) {
for (let j = 0; j < i; j++) {
// 方程:如果 nums[i] 比前面的大,尝试接在后面
if (nums[j] < nums[i]) dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
return Math.max(...dp); // 答案
};
// 139. 单词拆分
var wordBreak = function(s, wordDict) {
const n = s.length;
const wordSet = new Set(wordDict);
const dp = new Array(n + 1).fill(false); // 状态:前i个字符能否拆分
dp[0] = true; // 初始化:空字符串为true
for (let i = 1; i <= n; i++) {
for (let j = 0; j < i; j++) {
// 方程:如果前j个能拆分,且剩余部分在字典中
if (dp[j] && wordSet.has(s.substring(j, i))) {
dp[i] = true;
break;
}
}
}
return dp[n]; // 答案
};
```
---
### 模板二:完全背包DP
> **状态**:`dp[i]` = 凑成金额/容量 `i` 的最优解
> **特点**:物品可重复选取,外层遍历目标,内层遍历选择
```javascript
// 完全背包模板
const dp = new Array(amount + 1).fill(Infinity); // 1. 状态:凑成金额i的最优解
dp[0] = 0; // 2. 初始化
for (let i = 1; i <= amount; i++) { // 遍历容量
for (const coin of coins) { // 遍历选择
if (i >= coin) {
dp[i] = Math.min(dp[i], dp[i - coin] + 1); // 3. 转移方程
}
}
}
return dp[amount] > amount ? -1 : dp[amount]; // 4. 返回值
```
| 题目 | 题干 | 状态定义 | 转移方程 |
| :------------------------------------------------------------- | :------------------------------------- | :-------------------------- | :------------------------------ |
| [322. 零钱兑换](https://leetcode-cn.com/problems/coin-change/) | 凑成总金额 `amount` 所需的最少硬币个数 | `dp[i]` = 凑成i的最少硬币数 | `dp[i] = min(dp[i - coin] + 1)` |
代码实现 (点击展开)
```javascript
// 322. 零钱兑换
var coinChange = function(coins, amount) {
const dp = new Array(amount + 1).fill(Infinity); // 状态:凑成金额i的最少硬币数
dp[0] = 0; // 初始化
for (let i = 1; i <= amount; i++) {
for (const coin of coins) {
if (i >= coin) {
// 方程:取当前硬币,则需凑齐 i-coin 的金额
dp[i] = Math.min(dp[i], dp[i - coin] + 1);
}
}
}
return dp[amount] === Infinity ? -1 : dp[amount]; // 答案
};
```
---
### 模板三:双序列DP
> **状态**:`dp[i][j]` = `s1[0..i]` 与 `s2[0..j]` 的匹配结果
> **特点**:两个序列对比,根据末尾字符是否相等分情况
```javascript
// 双序列DP模板
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0)); // 1. 状态
// 2. 初始化边界 dp[0][j] 和 dp[i][0]
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (s1[i-1] === s2[j-1]) {
dp[i][j] = dp[i-1][j-1] + 1; // 3. 转移方程:匹配
} else {
dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]); // 3. 转移方程:不匹配
}
}
}
return dp[m][n]; // 4. 答案
```
| 题目 | 题干 | 状态定义 | 转移方程 |
| :----------------------------------------------------------------------------------- | :------------------------------------------------- | :---------------------- | :---------------------------------------------------------- |
| [1143. 最长公共子序列](https://leetcode-cn.com/problems/longest-common-subsequence/) | 返回两个字符串的最长公共子序列的长度 | `dp[i][j]` = LCS长度 | 相等:`dp[i-1][j-1]+1`;不等:`max(dp[i-1][j], dp[i][j-1])` |
| [72. 编辑距离](https://leetcode-cn.com/problems/edit-distance/) | 计算出将 `word1` 转换成 `word2` 所使用的最少操作数 | `dp[i][j]` = 最少操作数 | 相等:`dp[i-1][j-1]`;不等:`min(三方向) + 1` |
代码实现 (点击展开)
```javascript
// 1143. 最长公共子序列
var longestCommonSubsequence = function(text1, text2) {
const m = text1.length, n = text2.length;
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0)); // 状态:t1前i和t2前j的LCS
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (text1[i - 1] === text2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1; // 方程:字符相等,长度+1
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]); // 方程:不等,取左或上的最大值
}
}
}
return dp[m][n]; // 答案
};
// 72. 编辑距离
var minDistance = function(word1, word2) {
const m = word1.length, n = word2.length;
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0)); // 状态:w1前i转为w2前j的最小步数
for (let i = 0; i <= m; i++) dp[i][0] = i; // 初始化:w1变为空需删除i次
for (let j = 0; j <= n; j++) dp[0][j] = j; // 初始化:空变w2需插入j次
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (word1[i - 1] === word2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1]; // 方程:相等,无需操作
} else {
// 方程:不等,取 替换、删除、插入 三者最小值 + 1
dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]) + 1;
}
}
}
return dp[m][n]; // 答案
};
```
---
### 模板四:矩阵/区间DP
> **状态**:`dp[i][j]` = 从起点到 `(i,j)` / 区间 `[i,j]` 的最优解
> **特点**:矩阵按层遍历,区间从小到大枚举长度
```javascript
// 矩阵DP模板 (路径问题)
for (let i = 0; i < m; i++) {
for (let j = 0; j < n; j++) {
// 状态转移:通常依赖上方和左方
dp[i][j] = Math.min(dp[i-1][j], dp[i][j-1]) + grid[i][j];
}
}
// 区间DP模板 (回文/分割问题)
for (let len = 2; len <= n; len++) { // 1. 枚举区间长度
for (let i = 0; i + len - 1 < n; i++) { // 2. 枚举左端点
let j = i + len - 1; // 3. 计算右端点
// 4. 状态转移:根据子区间 [i+1, j-1] 等计算
dp[i][j] = 根据 dp[i+1][j-1] 等子区间计算;
}
}
```
| 题目 | 题干 | 状态定义 | 转移方程 |
| :--------------------------------------------------------------------------------- | :---------------------------------------------- | :------------------------------------- | :-------------------------------------- |
| [64. 最小路径和](https://leetcode-cn.com/problems/minimum-path-sum/) | 找出从左上角到右下角最小步数的路径和 | `dp[i][j]` = 到(i,j)的最小和 | `dp[i][j] = min(上, 左) + grid[i][j]` |
| [221. 最大正方形](https://leetcode-cn.com/problems/maximal-square/) | 找出矩阵中只包含 '1' 的最大正方形,并返回其面积 | `dp[i][j]` = 以(i,j)为右下角的最大边长 | `dp[i][j] = min(左,上,左上) + 1` |
| [5. 最长回文子串](https://leetcode-cn.com/problems/longest-palindromic-substring/) | 找到字符串 `s` 中最长的回文子串 | `dp[i][j]` = s[i..j]是否回文 | `dp[i][j] = s[i]==s[j] && dp[i+1][j-1]` |
代码实现 (点击展开)
```javascript
// 64. 最小路径和
var minPathSum = function(grid) {
const m = grid.length, n = grid[0].length;
const dp = Array.from({ length: m }, () => new Array(n).fill(0)); // 状态:到(i,j)的最小路径和
dp[0][0] = grid[0][0]; // 初始化起点
for (let i = 1; i < m; i++) dp[i][0] = dp[i - 1][0] + grid[i][0]; // 初始化第一列
for (let j = 1; j < n; j++) dp[0][j] = dp[0][j - 1] + grid[0][j]; // 初始化第一行
for (let i = 1; i < m; i++) {
for (let j = 1; j < n; j++) {
// 方程:只能从左边或上边过来
dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j];
}
}
return dp[m - 1][n - 1]; // 答案
};
// 221. 最大正方形
var maximalSquare = function(matrix) {
if (!matrix.length) return 0;
const m = matrix.length, n = matrix[0].length;
const dp = Array.from({ length: m }, () => new Array(n).fill(0)); // 状态:以(i,j)为右下角的最大正方形边长
let maxSide = 0;
for (let i = 0; i < m; i++) {
for (let j = 0; j < n; j++) {
if (matrix[i][j] === '1') {
if (i === 0 || j === 0) dp[i][j] = 1; // 边界初始化
else {
// 方程:受限于左、上、左上三个方向的最小值
dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]) + 1;
}
maxSide = Math.max(maxSide, dp[i][j]);
}
}
}
return maxSide * maxSide; // 答案:面积 = 边长平方
};
// 5. 最长回文子串
var longestPalindrome = function(s) {
const n = s.length;
if (n < 2) return s;
const dp = Array.from({ length: n }, () => new Array(n).fill(false)); // 状态:s[i..j]是否为回文
for (let i = 0; i < n; i++) dp[i][i] = true; // 初始化:单个字符必回文
let start = 0, maxLen = 1;
for (let len = 2; len <= n; len++) { // 枚举长度
for (let i = 0; i <= n - len; i++) { // 枚举左端点
let j = i + len - 1; // 计算右端点
if (s[i] === s[j]) {
// 方程:首尾相等且内部是回文(或内部为空/单字符)
if (len <= 3) dp[i][j] = true;
else dp[i][j] = dp[i + 1][j - 1];
}
if (dp[i][j] && len > maxLen) {
maxLen = len;
start = i;
}
}
}
return s.substring(start, start + maxLen); // 答案
};
```
---
## 五、 回溯算法 (Backtracking)
> **核心思想**:选择 → 递归 → 撤销(回溯)
> **时间复杂度**:通常 O(N!) 或 O(2^N),纯暴力穷举
### 回溯通用模板
```javascript
const result = [];
function backtrack(path, choices, start) {
// 1. 满足结束条件,收集结果
if (满足结束条件) {
result.push([...path]); // 注意拷贝,避免引用问题
return;
}
// 2. 遍历选择列表
for (let i = start; i < choices.length; i++) {
// 剪枝(可选):排除不合法的选择
if (需要剪枝) continue;
path.push(choices[i]); // 做选择:将选择加入路径
backtrack(path, choices, 下一个起点); // 递归:进入下一层决策树
path.pop(); // 撤销选择:回溯,恢复状态
}
}
```
### 三种经典场景对比
| 场景 | 下一层起点 | 是否需要visited | 去重方式 |
| -------- | --------------------------------- | --------------- | ----------------------------------- |
| **子集** | `i + 1` | ❌ | 排序 + `nums[i] === nums[i-1]` 跳过 |
| **组合** | `i + 1` (不可重复) / `i` (可重复) | ❌ | 同上 |
| **排列** | `0` (每次从头) | ✅ visited数组 | 排序 + `!visited[i-1]` 跳过 |
### 题目速查
| 题目 | 题干 | 类型 | 关键技巧 |
| :----------------------------------------------------------------------- | :--------------------------------------------------------- | :------- | :----------------------------------------------- |
| [46. 全排列](https://leetcode-cn.com/problems/permutations/) | 给定一个不含重复数字的数组,返回其所有可能的全排列 | 排列 | `visited` 数组标记已用元素 |
| [78. 子集](https://leetcode-cn.com/problems/subsets/) | 数组中元素互不相同,返回该数组所有可能的子集 | 子集 | 每个节点都收集结果 |
| [39. 组合总和](https://leetcode-cn.com/problems/combination-sum/) | 找出无重复元素数组中能凑成 `target` 的所有组合(可重复选) | 组合 | 可重复选,下一层从 `i` 开始 |
| [93. 复原IP地址](https://leetcode-cn.com/problems/restore-ip-addresses/) | 给定只包含数字的字符串,复原出所有可能的有效 IP 地址 | 分割 | 每段 1-3 位,值 ≤ 255,无前导零 |
| [22. 括号生成](https://leetcode-cn.com/problems/generate-parentheses/) | 生成所有可能的并且有效的括号组合 | 决策树 | `left < n` 可加左括号,`right < left` 可加右括号 |
| [79. 单词搜索](https://leetcode-cn.com/problems/word-search/) | 在网格中搜索是否存在给定的字符串单词 | 网格回溯 | 四方向 DFS + 原地标记访问 |
代码实现 (点击展开)
```javascript
// 46. 全排列
var permute = function(nums) {
const res = [];
const used = new Array(nums.length).fill(false);
const backtrack = (path) => {
if (path.length === nums.length) {
res.push([...path]); // 满足结束条件
return;
}
for (let i = 0; i < nums.length; i++) {
if (used[i]) continue; // 剪枝:已使用的元素跳过
used[i] = true;
path.push(nums[i]); // 做选择
backtrack(path); // 递归
path.pop(); // 撤销选择
used[i] = false;
}
};
backtrack([]);
return res;
};
// 78. 子集
var subsets = function(nums) {
const res = [];
const backtrack = (path, start) => {
res.push([...path]); // 每个节点都是一个子集
for (let i = start; i < nums.length; i++) {
path.push(nums[i]); // 做选择
backtrack(path, i + 1); // 递归:传入 i+1 保证不重复选
path.pop(); // 撤销选择
}
};
backtrack([], 0);
return res;
};
// 39. 组合总和
var combinationSum = function(candidates, target) {
const res = [];
const backtrack = (path, start, sum) => {
if (sum === target) {
res.push([...path]); // 满足结束条件
return;
}
if (sum > target) return; // 剪枝
for (let i = start; i < candidates.length; i++) {
path.push(candidates[i]);
// 递归:传入 i 而不是 i+1,表示元素可以重复选取
backtrack(path, i, sum + candidates[i]);
path.pop();
}
};
backtrack([], 0, 0);
return res;
};
// 93. 复原 IP 地址
var restoreIpAddresses = function(s) {
const res = [];
const backtrack = (path, start) => {
if (path.length === 4) {
if (start === s.length) res.push(path.join('.'));
return;
}
for (let len = 1; len <= 3; len++) {
if (start + len > s.length) break;
const segment = s.substring(start, start + len);
// 剪枝:不能有前导零,且值不能大于 255
if (len > 1 && segment[0] === '0') break;
if (len === 3 && parseInt(segment) > 255) break;
path.push(segment);
backtrack(path, start + len);
path.pop();
}
};
backtrack([], 0);
return res;
};
// 22. 括号生成
var generateParenthesis = function(n) {
const res = [];
const backtrack = (path, left, right) => {
if (path.length === 2 * n) {
res.push(path);
return;
}
// 剪枝:左括号随时加(只要不满n),右括号必须少于左括号时加
if (left < n) backtrack(path + '(', left + 1, right);
if (right < left) backtrack(path + ')', left, right + 1);
};
backtrack('', 0, 0);
return res;
};
// 79. 单词搜索
var exist = function(board, word) {
const m = board.length, n = board[0].length;
const backtrack = (i, j, k) => {
if (k === word.length) return true; // 找到单词
if (i < 0 || i >= m || j < 0 || j >= n || board[i][j] !== word[k]) return false;
const temp = board[i][j];
board[i][j] = '#'; // 标记已访问,防止回头
const found = backtrack(i + 1, j, k + 1) ||
backtrack(i - 1, j, k + 1) ||
backtrack(i, j + 1, k + 1) ||
backtrack(i, j - 1, k + 1);
board[i][j] = temp; // 回溯:恢复网格状态
return found;
};
for (let i = 0; i < m; i++) {
for (let j = 0; j < n; j++) {
if (backtrack(i, j, 0)) return true;
}
}
return false;
};
```
---
## 六、 搜索 (DFS/BFS)
### 网格DFS模板 (岛屿问题)
```javascript
function dfs(grid, i, j) {
// 1. 边界检查 + 访问检查
if (i < 0 || i >= m || j < 0 || j >= n) return 0;
if (grid[i][j] !== '1') return 0;
// 2. 标记已访问(原地修改,避免重复访问)
grid[i][j] = '0';
// 3. 四方向递归:上下左右
return 1 + dfs(grid, i+1, j) + dfs(grid, i-1, j)
+ dfs(grid, i, j+1) + dfs(grid, i, j-1);
}
// 主函数:遍历每个格子,寻找入口
for (let i = 0; i < m; i++) {
for (let j = 0; j < n; j++) {
if (grid[i][j] === '1') {
count++; // 发现新岛屿
dfs(grid, i, j); // 沉没整个岛屿
}
}
}
```
### BFS模板 (层序遍历)
```javascript
function bfs(grid, startI, startJ) {
const queue = [[startI, startJ]]; // 1. 初始化队列
const dirs = [[1,0], [-1,0], [0,1], [0,-1]];
while (queue.length) {
const [i, j] = queue.shift(); // 2. 弹出队头元素
for (const [di, dj] of dirs) {
const ni = i + di, nj = j + dj;
// 3. 检查边界与合法性
if (ni >= 0 && ni < m && nj >= 0 && nj < n && grid[ni][nj] === '1') {
grid[ni][nj] = '0'; // 4. 标记访问并入队
queue.push([ni, nj]);
}
}
}
}
```
### 题目速查
| 题目 | 题干 | 算法 | 关键技巧 |
| :------------------------------------------------------------------------------ | :--------------------------------------------------- | :------ | :------------------------------- |
| [200. 岛屿数量](https://leetcode-cn.com/problems/number-of-islands/) | 计算由 '1'(陆地)和 '0'(水)组成的网格中岛屿的数量 | DFS/BFS | 遇到 `'1'` 启动搜索,沉没整个岛 |
| [695. 岛屿的最大面积](https://leetcode-cn.com/problems/max-area-of-island/) | 计算并返回网格中岛屿的最大面积 | DFS | 返回递归面积,取最大值 |
| [240. 搜索二维矩阵 II](https://leetcode-cn.com/problems/search-a-2d-matrix-ii/) | 在行和列都升序排列的矩阵中搜索目标值 | 双指针 | 从右上角开始,大了往左,小了往下 |
代码实现 (点击展开)
```javascript
// 200. 岛屿数量 (套用 DFS 模板)
var numIslands = function(grid) {
if (!grid || grid.length === 0) return 0;
const m = grid.length, n = grid[0].length;
let count = 0;
const dfs = (i, j) => {
// 边界检查 & 检查是否为陆地
if (i < 0 || i >= m || j < 0 || j >= n || grid[i][j] === '0') return;
grid[i][j] = '0'; // 沉没当前陆地
// 四方向遍历
dfs(i + 1, j);
dfs(i - 1, j);
dfs(i, j + 1);
dfs(i, j - 1);
};
for (let i = 0; i < m; i++) {
for (let j = 0; j < n; j++) {
if (grid[i][j] === '1') {
count++; // 发现新岛屿
dfs(i, j); // 启动 DFS 将该岛屿彻底沉没
}
}
}
return count;
};
// 695. 岛屿的最大面积 (套用 DFS 模板 - 带返回值)
var maxAreaOfIsland = function(grid) {
const m = grid.length, n = grid[0].length;
let maxArea = 0;
const dfs = (i, j) => {
// 边界检查 & 检查是否为陆地
if (i < 0 || i >= m || j < 0 || j >= n || grid[i][j] !== 1) return 0;
grid[i][j] = 0; // 标记已访问 (修改为0)
// 当前面积(1) + 四周的面积
return 1 + dfs(i + 1, j) + dfs(i - 1, j) + dfs(i, j + 1) + dfs(i, j - 1);
};
for (let i = 0; i < m; i++) {
for (let j = 0; j < n; j++) {
if (grid[i][j] === 1) {
// 更新最大面积
maxArea = Math.max(maxArea, dfs(i, j));
}
}
}
return maxArea;
};
// 240. 搜索二维矩阵 II (类二分查找 / 聪明双指针)
// 虽然放在搜索章节,但这题用双指针更优:从右上角出发
var searchMatrix = function(matrix, target) {
if (!matrix.length) return false;
const m = matrix.length, n = matrix[0].length;
// 从右上角开始 (row = 0, col = n - 1)
let row = 0, col = n - 1;
while (row < m && col >= 0) {
const curr = matrix[row][col];
if (curr === target) {
return true;
} else if (curr > target) {
col--; // 当前值太大,往左移变小
} else {
row++; // 当前值太小,往下移变大
}
}
return false;
};
```
---
## 七、 二分查找 (Binary Search)
- [33. 搜索旋转排序数组](https://leetcode-cn.com/problems/search-in-rotated-sorted-array/) (Medium)
> 给你旋转后的数组 `nums` 和一个整数 `target` ,如果 `nums` 中存在这个目标值 ,则返回它的下标。
- [69. x 的平方根](https://leetcode-cn.com/problems/sqrtx/) (Easy)
> 给你一个非负整数 `x` ,计算并返回 `x` 的算术平方根。结果只保留整数部分。
- [162. 寻找峰值](https://leetcode-cn.com/problems/find-peak-element/) (Medium)
> 给你一个整数数组 `nums`,找到峰值元素并返回其索引。峰值元素是指其值大于左右相邻值的元素。
- [4. 寻找两个正序数组的中位数](https://leetcode-cn.com/problems/median-of-two-sorted-arrays/) (Hard)
> 给定两个大小分别为 `m` 和 `n` 的正序数组 `nums1` 和 `nums2`。请你找出这两个正序数组的中位数。
### 核心模板(推荐使用)
```js
// 通用二分模板 - 避免边界问题
function binarySearch(nums, target) {
let left = 0, right = nums.length - 1;
while (left + 1 < right) {
let mid = left + ((right - left) >> 1);
if (nums[mid] === target) {
return mid;
} else if (nums[mid] < target) {
left = mid;
} else {
right = mid;
}
}
// 后处理:检查剩余的两个元素
if (nums[left] === target) return left;
if (nums[right] === target) return right;
return -1;
}
```
### 33. 搜索旋转排序数组 (Medium)
**关键思路**:旋转数组一分为二,必有一半是有序的,判断 target 在哪一半。
```js
var search = function(nums, target) {
let left = 0, right = nums.length - 1;
while (left + 1 < right) {
let mid = left + ((right - left) >> 1);
if (nums[mid] === target) return mid;
// 判断哪半边有序
if (nums[left] < nums[mid]) {
// 左半边有序
if (nums[left] <= target && target <= nums[mid]) {
right = mid;
} else {
left = mid;
}
} else {
// 右半边有序
if (nums[mid] <= target && target <= nums[right]) {
left = mid;
} else {
right = mid;
}
}
}
if (nums[left] === target) return left;
if (nums[right] === target) return right;
return -1;
};
```
### 69. x 的平方根 (Easy)
**关键思路**:在 [0, x] 范围内二分查找,找最大的 k 使得 k² ≤ x。
```js
var mySqrt = function(x) {
if (x < 2) return x;
let left = 1, right = Math.floor(x / 2);
while (left + 1 < right) {
let mid = left + ((right - left) >> 1);
let square = mid * mid;
if (square === x) {
return mid;
} else if (square < x) {
left = mid;
} else {
right = mid;
}
}
// 取较大值能满足条件的那个
if (right * right <= x) return right;
return left;
};
```
### 162. 寻找峰值 (Medium)
**关键思路**:比较 mid 和 mid+1,往更大的方向走一定能找到峰值。
```js
var findPeakElement = function(nums) {
let left = 0, right = nums.length - 1;
while (left + 1 < right) {
let mid = left + ((right - left) >> 1);
if (nums[mid] > nums[mid + 1]) {
// 峰值在左边(包括mid)
right = mid;
} else {
// 峰值在右边
left = mid;
}
}
// 返回较大的那个
return nums[left] > nums[right] ? left : right;
};
```
### 4. 寻找两个正序数组的中位数 (Hard)
**关键思路**:二分查找分割点,使左半部分元素个数 = (m+n+1)/2,且左边最大值 ≤ 右边最小值。
```js
var findMedianSortedArrays = function(nums1, nums2) {
// 确保 nums1 是较短的数组
if (nums1.length > nums2.length) {
[nums1, nums2] = [nums2, nums1];
}
const m = nums1.length, n = nums2.length;
const halfLen = Math.floor((m + n + 1) / 2);
let left = 0, right = m;
while (left <= right) {
const i = left + ((right - left) >> 1); // nums1 的分割点
const j = halfLen - i; // nums2 的分割点
const nums1LeftMax = i === 0 ? -Infinity : nums1[i - 1];
const nums1RightMin = i === m ? Infinity : nums1[i];
const nums2LeftMax = j === 0 ? -Infinity : nums2[j - 1];
const nums2RightMin = j === n ? Infinity : nums2[j];
if (nums1LeftMax <= nums2RightMin && nums2LeftMax <= nums1RightMin) {
// 找到正确分割点
if ((m + n) % 2 === 1) {
return Math.max(nums1LeftMax, nums2LeftMax);
}
return (Math.max(nums1LeftMax, nums2LeftMax) +
Math.min(nums1RightMin, nums2RightMin)) / 2;
} else if (nums1LeftMax > nums2RightMin) {
right = i - 1; // nums1 分割点左移
} else {
left = i + 1; // nums1 分割点右移
}
}
return 0;
};
```
### 总结对比
| 题目 | 题干 | 难点 | 二分条件 |
| :----------- | :----------------------------- | :----------- | :------------------------ |
| 33. 旋转数组 | 在旋转后的升序数组中搜索目标值 | 判断有序半边 | `nums[left] < nums[mid]` |
| 69. 平方根 | 计算非负整数 x 的算术平方根 | 边界处理 | `mid * mid` 与 x 比较 |
| 162. 峰值 | 在数组中寻找任意一个峰值索引 | 方向选择 | `nums[mid] > nums[mid+1]` |
| 4. 中位数 | 找两个正序数组的中位数 | 双数组分割 | 分割点满足交叉条件 |
---
## 八、 栈/字符串/数学/其他
**栈**
- [20. 有效的括号](https://leetcode-cn.com/problems/valid-parentheses/) (Easy)
> 给定一个只包括括号的字符串,判断字符串是否有效(左括号必须以正确顺序闭合)。
- [32. 最长有效括号](https://leetcode-cn.com/problems/longest-valid-parentheses/) (Hard)
> 给你一个只包含 '(' 和 ')' 的字符串,找出最长有效(格式正确且连续)括号子串的长度。
- [155. 最小栈](https://leetcode-cn.com/problems/min-stack/) (Easy)
> 设计一个支持 `push`, `pop`, `top` 操作,并能在常数时间内检索到最小元素的栈。
- [232. 用栈实现队列](https://leetcode-cn.com/problems/implement-queue-using-stacks/) (Easy)
> 请你仅使用两个栈实现先入先出队列。
- [394. 字符串解码](https://leetcode-cn.com/problems/decode-string/) (Medium)
> 给定一个经过编码的字符串(如 `3[a]2[bc]`),返回它解码后的字符串(`aaabcbc`)。
### 栈的通用思路
栈题目没有固定模板,但有通用思路:
| 场景 | 思路 | 栈中存储 |
| ------------ | -------------------------- | ------------- |
| 匹配问题 | 遇"左"入栈,遇"右"出栈匹配 | 字符 |
| 计算区间长度 | 存索引而非值 | 索引 |
| 维护额外信息 | 辅助栈同步维护 | 值 + 辅助信息 |
| 逆序处理 | 双栈倒腾 | 分离存储 |
### 20. 有效的括号 (Easy)
**思路**:遇到左括号入栈,遇到右括号检查栈顶是否匹配。
```js
var isValid = function(s) {
const stack = [];
const map = { ')': '(', ']': '[', '}': '{' };
for (const char of s) {
if (char === '(' || char === '[' || char === '{') {
stack.push(char);
} else {
if (stack.pop() !== map[char]) return false;
}
}
return stack.length === 0;
};
```
### 32. 最长有效括号 (Hard)
**思路**:栈中存索引,栈底保持"最后一个未匹配的右括号索引"作为分隔符。
```js
var longestValidParentheses = function(s) {
let maxLen = 0;
const stack = [-1]; // 初始放-1作为分隔符
for (let i = 0; i < s.length; i++) {
if (s[i] === '(') {
stack.push(i);
} else {
stack.pop();
if (stack.length === 0) {
stack.push(i); // 当前右括号作为新的分隔符
} else {
maxLen = Math.max(maxLen, i - stack[stack.length - 1]);
}
}
}
return maxLen;
};
```
### 155. 最小栈 (Easy)
**思路**:辅助栈同步记录当前最小值。
```js
var MinStack = function() {
this.stack = [];
this.minStack = [Infinity];
};
MinStack.prototype.push = function(val) {
this.stack.push(val);
this.minStack.push(Math.min(this.minStack[this.minStack.length - 1], val));
};
MinStack.prototype.pop = function() {
this.stack.pop();
this.minStack.pop();
};
MinStack.prototype.top = function() {
return this.stack[this.stack.length - 1];
};
MinStack.prototype.getMin = function() {
return this.minStack[this.minStack.length - 1];
};
```
### 232. 用栈实现队列 (Easy)
**思路**:双栈实现,输入栈负责 push,输出栈负责 pop/peek,输出栈空时从输入栈倒入。
```js
var MyQueue = function() {
this.inStack = [];
this.outStack = [];
};
MyQueue.prototype.push = function(x) {
this.inStack.push(x);
};
MyQueue.prototype.pop = function() {
if (this.outStack.length === 0) {
while (this.inStack.length) {
this.outStack.push(this.inStack.pop());
}
}
return this.outStack.pop();
};
MyQueue.prototype.peek = function() {
if (this.outStack.length === 0) {
while (this.inStack.length) {
this.outStack.push(this.inStack.pop());
}
}
return this.outStack[this.outStack.length - 1];
};
MyQueue.prototype.empty = function() {
return this.inStack.length === 0 && this.outStack.length === 0;
};
```
### 394. 字符串解码 (Medium)
**思路**:双栈分别存数字和字符串,遇到 `]` 时弹出拼接。
```js
var decodeString = function(s) {
const numStack = [];
const strStack = [];
let num = 0;
let str = '';
for (const char of s) {
if (char >= '0' && char <= '9') {
num = num * 10 + Number(char);
} else if (char === '[') {
numStack.push(num);
strStack.push(str);
num = 0;
str = '';
} else if (char === ']') {
const repeatTimes = numStack.pop();
str = strStack.pop() + str.repeat(repeatTimes);
} else {
str += char;
}
}
return str;
};
```
**排序**
- [215. 数组中的第K个最大元素](https://leetcode-cn.com/problems/kth-largest-element-in-an-array/) (Medium) - *快速选择*
> 给定整数数组 `nums` 和整数 `k`,请返回数组中第 `k` 个最大的元素。
- [补充题4. 手撕快速排序](https://leetcode-cn.com/problems/kth-largest-element-in-an-array/)
> 给定一个数组,实现快速排序算法。
### 快速排序模板
```js
function quickSort(nums, left = 0, right = nums.length - 1) {
if (left < right) {
const pivotIndex = partition(nums, left, right);
quickSort(nums, left, pivotIndex - 1);
quickSort(nums, pivotIndex + 1, right);
}
return nums;
}
function partition(nums, left, right) {
const pivot = nums[right]; // 选最右为基准
let i = left;
for (let j = left; j < right; j++) {
if (nums[j] < pivot) {
[nums[i], nums[j]] = [nums[j], nums[i]];
i++;
}
}
[nums[i], nums[right]] = [nums[right], nums[i]];
return i;
}
```
### 快速选择模板(找第K大/小)
**核心思想**:快排的 partition 每次确定一个元素的最终位置,只递归需要的那一半,时间复杂度 O(n)。
```js
function quickSelect(nums, left, right, k) {
if (left === right) return nums[left];
const pivotIndex = partition(nums, left, right);
if (pivotIndex === k) {
return nums[k];
} else if (pivotIndex < k) {
return quickSelect(nums, pivotIndex + 1, right, k);
} else {
return quickSelect(nums, left, pivotIndex - 1, k);
}
}
```
### 215. 数组中的第K个最大元素 (Medium)
**思路**:第 K 大 = 第 (n-k) 小,用快速选择。
```js
var findKthLargest = function(nums, k) {
const targetIndex = nums.length - k; // 第K大 = 排序后索引为 n-k
return quickSelect(nums, 0, nums.length - 1, targetIndex);
};
function quickSelect(nums, left, right, k) {
if (left === right) return nums[left];
const pivotIndex = partition(nums, left, right);
if (pivotIndex === k) {
return nums[k];
} else if (pivotIndex < k) {
return quickSelect(nums, pivotIndex + 1, right, k);
} else {
return quickSelect(nums, left, pivotIndex - 1, k);
}
}
function partition(nums, left, right) {
// 随机选择基准,避免最坏情况
const randomIndex = left + Math.floor(Math.random() * (right - left + 1));
[nums[randomIndex], nums[right]] = [nums[right], nums[randomIndex]];
const pivot = nums[right];
let i = left;
for (let j = left; j < right; j++) {
if (nums[j] < pivot) {
[nums[i], nums[j]] = [nums[j], nums[i]];
i++;
}
}
[nums[i], nums[right]] = [nums[right], nums[i]];
return i;
}
```
### 排序算法对比
| 算法 | 时间复杂度 | 空间复杂度 | 稳定性 | 适用场景 |
| -------- | ------------- | ---------- | ------ | ------------------ |
| 快速排序 | O(nlogn) 平均 | O(logn) | 不稳定 | 通用排序 |
| 快速选择 | O(n) 平均 | O(1) | - | 找第K大/小 |
| 堆排序 | O(nlogn) | O(1) | 不稳定 | TopK问题 |
| 归并排序 | O(nlogn) | O(n) | 稳定 | 链表排序、求逆序对 |
**字符串/数学**
- [415. 字符串相加](https://leetcode-cn.com/problems/add-strings/) (Easy)
> 给定两个字符串形式的非负整数 `num1` 和 `num2` ,计算它们的和并输出。
- [43. 字符串相乘](https://leetcode-cn.com/problems/multiply-strings/) (Medium)
> 给定两个以字符串形式表示的非负整数 `num1` 和 `num2`,返回 `num1` 和 `num2` 的乘积。
- [165. 比较版本号](https://leetcode-cn.com/problems/compare-version-numbers/) (Medium)
> 如果 `version1 > version2` 返回 1,反之返回 -1,相等返回 0(处理如 `1.0.1` 和 `1` 的比较)。
- [470. 用 Rand7() 实现 Rand10()](https://leetcode-cn.com/problems/implement-rand10-using-rand7/) (Medium)
> 已有方法 `rand7` 可生成 1 到 7 范围内的均匀随机整数,请实现 `rand10`。
- [440. 字典序的第K小数字](https://leetcode-cn.com/problems/k-th-smallest-in-lexicographical-order/) (Hard)
> 给你两个整数 `n` 和 `k` ,找到 1 到 `n` 字典序第 `k` 小的整数。
### 模板一:大数运算 (模拟竖式)
> **核心思想**:从字符串末尾(最低位)开始逐位操作,维护 `carry` 进位。对于乘法,`num1[i] * num2[j]` 的结果会叠加到 `res[i+j]` 和 `res[i+j+1]` 位置。
#### 415. 字符串相加 (Easy)
```javascript
/*
* 模板:双指针倒序遍历 + Carry处理
*/
var addStrings = function(num1, num2) {
let i = num1.length - 1, j = num2.length - 1, carry = 0;
const res = [];
while (i >= 0 || j >= 0 || carry !== 0) {
// 如果指针越界,视为 0
const x = i >= 0 ? num1.charAt(i--) - '0' : 0;
const y = j >= 0 ? num2.charAt(j--) - '0' : 0;
const sum = x + y + carry;
res.push(sum % 10);
carry = Math.floor(sum / 10);
}
return res.reverse().join('');
};
```
#### 43. 字符串相乘 (Medium)
```javascript
/*
* 模板:乘积位置规律
* num1[i] * num2[j] 会影响 res[i + j] 和 res[i + j + 1]
*/
var multiply = function(num1, num2) {
if (num1 === '0' || num2 === '0') return '0';
const m = num1.length, n = num2.length;
const res = new Array(m + n).fill(0);
// 从个位开始相乘
for (let i = m - 1; i >= 0; i--) {
for (let j = n - 1; j >= 0; j--) {
const mul = (num1[i] - '0') * (num2[j] - '0');
const p1 = i + j; // 进位位置
const p2 = i + j + 1; // 当前位位置
const sum = mul + res[p2];
res[p2] = sum % 10;
res[p1] += Math.floor(sum / 10); // 进位累加到 p1
}
}
// 去除前导零 (如 9*9=81,res长度为2,0位置非0;但有些情况可能有多余前导0)
while (res[0] === 0) res.shift();
return res.join('');
};
```
### 模板二:双指针分块解析
> **核心思想**:处理 "分块" 字符串(如 IP 地址、版本号)。使用 `while` 循环解析当前块的数值,遇到分隔符停下,比较后跳过分隔符继续。
#### 165. 比较版本号 (Medium)
```javascript
var compareVersion = function(version1, version2) {
let p1 = 0, p2 = 0;
const n1 = version1.length, n2 = version2.length;
while (p1 < n1 || p2 < n2) {
let num1 = 0, num2 = 0;
// 解析 v1 的当前块
while (p1 < n1 && version1[p1] !== '.') {
num1 = num1 * 10 + (version1[p1++] - '0');
}
// 解析 v2 的当前块
while (p2 < n2 && version2[p2] !== '.') {
num2 = num2 * 10 + (version2[p2++] - '0');
}
if (num1 > num2) return 1;
if (num1 < num2) return -1;
// 跳过点,进入下一块
p1++; p2++;
}
return 0;
};
```
### 模板三:拒绝采样 (概率)
> **通用公式**:`(randX() - 1) * Y + randY()` 可以生成 `1` 到 `X*Y` 的均匀随机整数。
> **核心思想**:生成一个比目标范围大的均匀分布,如果生成的数落在目标范围内则返回,否则拒绝并重试。
#### 470. 用 Rand7() 实现 Rand10() (Medium)
```javascript
var rand10 = function() {
while (true) {
// (rand7() - 1) * 7 + rand7() -> 生成 1 到 49 的均匀整数
const num = (rand7() - 1) * 7 + rand7();
// 只要 1-40 的数 (40是10的倍数,可以均匀映射)
if (num <= 40) {
return (num - 1) % 10 + 1;
}
// 大于 40 的数拒绝,进入下一轮循环
}
};
```
### 模板四:字典序计数 (十叉树)
> **核心思想**:将 `1` 到 `n` 的数字看作一棵 **十叉树**(前序遍历即字典序)。
> 为了找到第 `K` 小,我们需要决定是 **"向右走"** (跨过当前子树) 还是 **"向下走"** (进入子树)。
> - 计算 `curr` 下面的子节点数 `steps`。
> - 若 `steps <= k`:说明目标不在这个子树,`curr++` (向右),`k -= steps`。
> - 若 `steps > k`:说明目标在这个子树内,`curr *= 10` (向下),`k--` (减去根节点)。
#### 440. 字典序的第K小数字 (Hard)
```javascript
var findKthNumber = function(n, k) {
let curr = 1;
k--; // k 指还需要跳过的节点数(扣除起点 1)
while (k > 0) {
const steps = getSteps(curr, n);
if (steps <= k) {
curr++; // 向右走:去兄弟节点
k -= steps; // 减去整个子树的节点数
} else {
curr *= 10; // 向下走:去第一个子节点
k--; // 减去当前根节点这 1 个计数
}
}
return curr;
};
// 计算以 curr 为根的子树节点数(不大于 n)
function getSteps(curr, n) {
let steps = 0;
let first = curr;
let last = curr;
while (first <= n) {
// 当前层有多少个节点:min(last, n) - first + 1
steps += Math.min(last, n) - first + 1;
first *= 10;
last = last * 10 + 9;
}
return steps;
}
```