基础操作
-
206. 反转链表 (Easy) - 必背
给你单链表的头节点
head,请你反转链表,并返回反转后的链表。 -
92. 反转链表 II (Medium) - 区间反转
给你单链表的头节点
head和两个整数left和right,请你反转从位置left到位置right的链表节点,返回反转后的链表。 -
25. K 个一组翻转链表 (Hard) - 面试常客
给你一个链表,每
k个节点一组进行翻转,请你返回翻转后的链表。 -
21. 合并两个有序链表 (Easy)
将两个升序链表合并为一个新的升序链表并返回。
-
23. 合并K个排序链表 (Hard) - 堆/归并
给你一个链表数组,每个链表都已经按升序排列。请你将所有链表合并到一个升序链表中。
-
148. 排序链表 (Medium) - 归并排序
给你链表的头结点
head,请将其按升序排列并返回排序后的链表(要求$O(n \log n)$ 时间复杂度和$O(1)$ 空间复杂度)。 -
补充题1. 排序奇升偶降链表 (Medium)
给定一个奇数位升序,偶数位降序的链表,将其排序为升序。 (思路:拆分、反转偶数链表、合并)
双指针技巧
- 141. 环形链表 (Easy) - 判圈
给你一个链表的头节点
head,判断链表中是否有环。 - 142. 环形链表 II (Medium) - 找入口
给定一个链表,返回链表开始入环的第一个节点。如果链表无环,则返回
null。 - 160. 相交链表 (Easy)
给你两个单链表的头节点
headA和headB,请你找出并返回两个单链表相交的起始节点。 - 19. 删除链表的倒数第N个节点 (Medium)
给你一个链表,删除链表的倒数第
n个结点,并且返回链表的头结点。 - 剑指 Offer 22. 链表中倒数第k个节点 (Easy)
输入一个链表,输出该链表中倒数第
k个节点。
综合/技巧
-
143. 重排链表 (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. 两数相加 (Medium)
给你两个非空的链表,表示两个非负的整数。它们每位数字都是按照逆序的方式存储的。请你将两个数相加。
-
146. LRU缓存机制 (Medium) - 双向链表+哈希
设计并实现一个满足 LRU (最近最少使用) 缓存约束的数据结构。
-
82. 删除排序链表中的重复元素 II (Medium)
给定一个已排序的链表的头
head,删除所有含有重复数字的节点,只保留原始链表中未重复出现的数字。
遍历 (BFS/DFS)
- 102. 二叉树的层序遍历 (Medium) - BFS模板
给你二叉树的根节点
root,返回其节点值的层序遍历(即逐层地,从左到右访问所有节点)。 - 103. 二叉树的锯齿形层次遍历 (Medium)
给你二叉树的根节点
root,返回其节点值的锯齿形层序遍历(先从左往右,下一层再从右往左,以此类推,层与层之间交替进行)。 - 199. 二叉树的右视图 (Medium)
给定一个二叉树的根节点
root,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。 - 662. 二叉树最大宽度 (Medium)
给定一个二叉树,编写一个函数来获取这个树的最大宽度。每一层的宽度被定义为两个端点之间的长度。
- 94. 二叉树的中序遍历 (Easy)
给你二叉树的根节点
root,返回它节点值的中序遍历。
路径与属性 (分治思维)
-
101. 对称二叉树 (Easy)
给你一个二叉树的根节点
root,检查它是否轴对称。 -
105. 从前序与中序遍历序列构造二叉树 (Medium)
给定两个整数数组
preorder和inorder,请构造二叉树并返回其根节点。 -
236. 二叉树的最近公共祖先 (Medium) - 必考
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
-
124. 二叉树中的最大路径和 (Hard)
二叉树中的最大路径和是指路径上节点值的最大和。路径可以是任何节点作为起点和终点。
-
112. 路径总和 (Easy)
给你二叉树的根节点
root和一个表示目标和的整数targetSum。判断该树中是否存在根节点到叶子节点的路径且和等于目标和。 -
113. 路径总和 II (Medium)
给你二叉树的根节点
root和一个整数targetSum,找出所有从根节点到叶子节点路径总和等于给定目标和的路径。 -
129. 求根到叶子节点数字之和 (Medium)
计算从根节点到叶子节点生成的所有数字之和。每条路径代表一个数字(如
$1 \to 2 \to 3$ 代表$123$ )。 -
剑指 Offer 26. 树的子结构 (Medium)
输入两棵二叉树A和B,判断B是不是A的子结构。
二叉搜索树 (BST)
- 98. 验证二叉搜索树 (Medium)
给你一个二叉树的根节点
root,判断其是否是一个有效的二叉搜索树。
基础双指针 (Two Pointers Basics)
-
167. 两数之和 II (Easy) - 左右指针
给你一个已按 非递减顺序排列 的整数数组
numbers,请你从数组中找出两个数满足相加之和等于目标数target。 -
15. 三数之和 (Medium) - 排序+双指针
给你一个包含
n个整数的数组nums,判断nums中是否存在三个元素$a, b, c$ ,使得$a + b + c = 0$ ?找出所有和为 0 且不重复的三元组。 -
42. 接雨水 (Hard) - 必考
给定
n个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨后能接多少雨水。
滑动窗口 (Sliding Window)
-
3. 无重复字符的最长子串 (Medium) - 模版题
给定一个字符串
s,请你找出其中不含有重复字符的 最长子串 的长度。 -
209. 长度最小的子数组 (Medium)
给定一个含有
n个正整数的数组和一个正整数target。找出该数组中满足其和$\ge target$ 的长度最小的 连续子数组。 -
76. 最小覆盖子串 (Hard)
给你一个字符串
s、一个字符串t。返回s中包含t所有字符的最小子串。 -
239. 滑动窗口最大值 (Hard) - 单调队列
给你一个整数数组
nums,有一个大小为k的滑动窗口从数组的最左侧移动到数组的最右侧。返回滑动窗口中的最大值。
对撞双指针 (左右向中间逼近)
- 42. 接雨水 (Hard) - 必考,左右指针+维护最大高度
(同上,描述见上文)
- 88. 合并两个有序数组 (Easy) - 逆向双指针,从后往前填充
给你两个按非降序排列的整数数组
nums1和nums2,请你将nums2合并到nums1中,使合并后的数组同样按非降序排列。 - 11. 盛最多水的容器 (Medium) - 移动较短的一边
给定一个长度为
n的整数数组height。找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。返回最大水量。
快慢指针 (同向不同速)
-
26. 删除有序数组中的重复项 (Easy) - slow记录有效位置
给你一个非降序排列的数组
nums,请你 原地 删除重复出现的元素,并在数组的每个元素只出现一次的情况下,返回删除后数组的新长度。 -
27. 移除元素 (Easy) - 原地删除
给你一个数组
nums和一个值val,你需要 原地 移除所有数值等于val的元素,并返回移除后数组的新长度。 -
283. 移动零 (Easy) - 非零前移,尾部填零
给定一个数组
nums,编写一个函数将所有0移动到数组的末尾,同时保持非零元素的相对顺序。 -
287. 寻找重复数 (Medium) - Floyd判圈法
给定一个包含
n + 1个整数的数组nums,其数字都在[1, n]范围内,可知至少存在一个重复的整数。找出这个重复的数(不修改数组,使用$O(1)$ 额外空间)。
贪心策略
- 121. 买卖股票的最佳时机 (Easy) - 维护历史最低价
给定一个数组
prices,其中prices[i]表示一支给定股票第i天的价格。如果你最多只允许完成一笔交易(即买入和卖出一支股票一次),设计一个算法来计算你所能获取的最大利润。 - 122. 买卖股票的最佳时机 II (Medium) - 累加所有正收益
给你一个整数数组
prices,其中prices[i]表示某支股票第i天的价格。在每一天,你可以决定是否购买和/或出售股票。计算你所能获得的最大利润。
核心思想:
- 单次交易 (121):贪心地假设自己是在历史最低点买入的。遍历时持续更新“历史最低价 (
minPrice)”,并计算“当前价格卖出能赚多少 (price - minPrice)”,取最大值。- 无限次交易 (122):贪心地收集所有“上坡”的收益。只要今天的价格比昨天高,我就假装昨天买今天卖(
prices[i] - prices[i-1]),把所有正收益累加起来就是最大利润。
代码实现 (点击展开)
// 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. 螺旋矩阵 (Medium) - 四边界收缩
给你一个
m行n列的矩阵matrix,请按照顺时针螺旋顺序,返回矩阵中的所有元素。 -
48. 旋转图像 (Medium) - 先转置后翻转 / 四角轮换
给定一个
$n \times n$ 的二维矩阵matrix表示一个图像。请你将图像顺时针旋转 90 度(原地旋转)。 -
31. 下一个排列 (Medium) - 规律模拟:找降序起点
给你一个整数数组
nums,找出nums的下一个字典序更大的排列。
核心思想:
- 矩阵操作:通常不需要复杂算法,而是需要精确控制边界 (Top/Bottom/Left/Right) 或者利用几何数学变换 (转置+镜像)。
- 排列问题:观察数字规律。涉及顺序变换时,往往从倒序遍历找那个“破坏单调性”的点开始。
代码实现 (点击展开)
// 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. 合并区间 (Medium) - 排序后贪心合并
以数组
intervals表示若干个区间的集合,合并所有重叠的区间,并返回一个不重叠的区间数组。 -
41. 缺失的第一个正数 (Hard) - 原地哈希:nums[i]放到i-1位置
给你一个未排序的整数数组
nums,请你找出其中没有出现的最小的正整数(要求$O(n)$ 时间复杂度和$O(1)$ 空间复杂度)。
DP四要素:① 状态定义 → ② 转移方程 → ③ 初始化 → ④ 返回值
状态:
dp[i]= 以nums[i]结尾/到达位置i的最优解
特点:当前状态只依赖前面的状态
// 通用模板
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. 爬楼梯 | 需要 n 阶到达楼顶,每次可爬 1 或 2 阶,求方法数 | dp[i] = 到第i阶的方法数 |
dp[i] = dp[i-1] + dp[i-2] |
| 53. 最大子数组和 | 找出具有最大和的 连续子数组,返回其最大和 | dp[i] = 以i结尾的最大和 |
dp[i] = max(dp[i-1] + nums[i], nums[i]) |
| 300. 最长上升子序列 | 找到其中最长严格递增子序列的长度 | dp[i] = 以i结尾的LIS长度 |
dp[i] = max(dp[j] + 1) 其中 j < i && nums[j] < nums[i] |
| 139. 单词拆分 | 判定字符串 s 是否可以由 wordDict 中的单词拼接而成 |
dp[i] = 前i个字符能否拆分 |
dp[i] = dp[j] && s[j:i] in dict |
代码实现 (点击展开)
// 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[i]= 凑成金额/容量i的最优解
特点:物品可重复选取,外层遍历目标,内层遍历选择
// 完全背包模板
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. 零钱兑换 | 凑成总金额 amount 所需的最少硬币个数 |
dp[i] = 凑成i的最少硬币数 |
dp[i] = min(dp[i - coin] + 1) |
代码实现 (点击展开)
// 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[i][j]=s1[0..i]与s2[0..j]的匹配结果
特点:两个序列对比,根据末尾字符是否相等分情况
// 双序列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. 最长公共子序列 | 返回两个字符串的最长公共子序列的长度 | dp[i][j] = LCS长度 |
相等:dp[i-1][j-1]+1;不等:max(dp[i-1][j], dp[i][j-1]) |
| 72. 编辑距离 | 计算出将 word1 转换成 word2 所使用的最少操作数 |
dp[i][j] = 最少操作数 |
相等:dp[i-1][j-1];不等:min(三方向) + 1 |
代码实现 (点击展开)
// 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[i][j]= 从起点到(i,j)/ 区间[i,j]的最优解
特点:矩阵按层遍历,区间从小到大枚举长度
// 矩阵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. 最小路径和 | 找出从左上角到右下角最小步数的路径和 | dp[i][j] = 到(i,j)的最小和 |
dp[i][j] = min(上, 左) + grid[i][j] |
| 221. 最大正方形 | 找出矩阵中只包含 '1' 的最大正方形,并返回其面积 | dp[i][j] = 以(i,j)为右下角的最大边长 |
dp[i][j] = min(左,上,左上) + 1 |
| 5. 最长回文子串 | 找到字符串 s 中最长的回文子串 |
dp[i][j] = s[i..j]是否回文 |
dp[i][j] = s[i]==s[j] && dp[i+1][j-1] |
代码实现 (点击展开)
// 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); // 答案
};核心思想:选择 → 递归 → 撤销(回溯)
时间复杂度:通常 O(N!) 或 O(2^N),纯暴力穷举
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. 全排列 | 给定一个不含重复数字的数组,返回其所有可能的全排列 | 排列 | visited 数组标记已用元素 |
| 78. 子集 | 数组中元素互不相同,返回该数组所有可能的子集 | 子集 | 每个节点都收集结果 |
| 39. 组合总和 | 找出无重复元素数组中能凑成 target 的所有组合(可重复选) |
组合 | 可重复选,下一层从 i 开始 |
| 93. 复原IP地址 | 给定只包含数字的字符串,复原出所有可能的有效 IP 地址 | 分割 | 每段 1-3 位,值 ≤ 255,无前导零 |
| 22. 括号生成 | 生成所有可能的并且有效的括号组合 | 决策树 | left < n 可加左括号,right < left 可加右括号 |
| 79. 单词搜索 | 在网格中搜索是否存在给定的字符串单词 | 网格回溯 | 四方向 DFS + 原地标记访问 |
代码实现 (点击展开)
// 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;
};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); // 沉没整个岛屿
}
}
}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. 岛屿数量 | 计算由 '1'(陆地)和 '0'(水)组成的网格中岛屿的数量 | DFS/BFS | 遇到 '1' 启动搜索,沉没整个岛 |
| 695. 岛屿的最大面积 | 计算并返回网格中岛屿的最大面积 | DFS | 返回递归面积,取最大值 |
| 240. 搜索二维矩阵 II | 在行和列都升序排列的矩阵中搜索目标值 | 双指针 | 从右上角开始,大了往左,小了往下 |
代码实现 (点击展开)
// 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;
};- 33. 搜索旋转排序数组 (Medium)
给你旋转后的数组
nums和一个整数target,如果nums中存在这个目标值 ,则返回它的下标。 - 69. x 的平方根 (Easy)
给你一个非负整数
x,计算并返回x的算术平方根。结果只保留整数部分。 - 162. 寻找峰值 (Medium)
给你一个整数数组
nums,找到峰值元素并返回其索引。峰值元素是指其值大于左右相邻值的元素。 - 4. 寻找两个正序数组的中位数 (Hard)
给定两个大小分别为
m和n的正序数组nums1和nums2。请你找出这两个正序数组的中位数。
// 通用二分模板 - 避免边界问题
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;
}关键思路:旋转数组一分为二,必有一半是有序的,判断 target 在哪一半。
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;
};关键思路:在 [0, x] 范围内二分查找,找最大的 k 使得 k² ≤ x。
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;
};关键思路:比较 mid 和 mid+1,往更大的方向走一定能找到峰值。
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;
};关键思路:二分查找分割点,使左半部分元素个数 = (m+n+1)/2,且左边最大值 ≤ 右边最小值。
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. 有效的括号 (Easy)
给定一个只包括括号的字符串,判断字符串是否有效(左括号必须以正确顺序闭合)。
- 32. 最长有效括号 (Hard)
给你一个只包含 '(' 和 ')' 的字符串,找出最长有效(格式正确且连续)括号子串的长度。
- 155. 最小栈 (Easy)
设计一个支持
push,pop,top操作,并能在常数时间内检索到最小元素的栈。 - 232. 用栈实现队列 (Easy)
请你仅使用两个栈实现先入先出队列。
- 394. 字符串解码 (Medium)
给定一个经过编码的字符串(如
3[a]2[bc]),返回它解码后的字符串(aaabcbc)。
栈题目没有固定模板,但有通用思路:
| 场景 | 思路 | 栈中存储 |
|---|---|---|
| 匹配问题 | 遇"左"入栈,遇"右"出栈匹配 | 字符 |
| 计算区间长度 | 存索引而非值 | 索引 |
| 维护额外信息 | 辅助栈同步维护 | 值 + 辅助信息 |
| 逆序处理 | 双栈倒腾 | 分离存储 |
思路:遇到左括号入栈,遇到右括号检查栈顶是否匹配。
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;
};思路:栈中存索引,栈底保持"最后一个未匹配的右括号索引"作为分隔符。
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;
};思路:辅助栈同步记录当前最小值。
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];
};思路:双栈实现,输入栈负责 push,输出栈负责 pop/peek,输出栈空时从输入栈倒入。
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;
};思路:双栈分别存数字和字符串,遇到 ] 时弹出拼接。
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个最大元素 (Medium) - 快速选择
给定整数数组
nums和整数k,请返回数组中第k个最大的元素。 - 补充题4. 手撕快速排序
给定一个数组,实现快速排序算法。
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;
}核心思想:快排的 partition 每次确定一个元素的最终位置,只递归需要的那一半,时间复杂度 O(n)。
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);
}
}思路:第 K 大 = 第 (n-k) 小,用快速选择。
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. 字符串相加 (Easy)
给定两个字符串形式的非负整数
num1和num2,计算它们的和并输出。 - 43. 字符串相乘 (Medium)
给定两个以字符串形式表示的非负整数
num1和num2,返回num1和num2的乘积。 - 165. 比较版本号 (Medium)
如果
version1 > version2返回 1,反之返回 -1,相等返回 0(处理如1.0.1和1的比较)。 - 470. 用 Rand7() 实现 Rand10() (Medium)
已有方法
rand7可生成 1 到 7 范围内的均匀随机整数,请实现rand10。 - 440. 字典序的第K小数字 (Hard)
给你两个整数
n和k,找到 1 到n字典序第k小的整数。
核心思想:从字符串末尾(最低位)开始逐位操作,维护
carry进位。对于乘法,num1[i] * num2[j]的结果会叠加到res[i+j]和res[i+j+1]位置。
/*
* 模板:双指针倒序遍历 + 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('');
};/*
* 模板:乘积位置规律
* 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循环解析当前块的数值,遇到分隔符停下,比较后跳过分隔符继续。
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的均匀随机整数。 核心思想:生成一个比目标范围大的均匀分布,如果生成的数落在目标范围内则返回,否则拒绝并重试。
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--(减去根节点)。
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;
}