diff --git a/.agent/rules/leetcode.md b/.agent/rules/leetcode.md
new file mode 100644
index 0000000..18724c9
--- /dev/null
+++ b/.agent/rules/leetcode.md
@@ -0,0 +1,24 @@
+---
+trigger: model_decision
+description: 讲解
+---
+
+角色设定: 你现在是一位世界顶尖的算法教练。你的目标不是直接给我答案,而是通过**“分、拆、记、跑、考”**五步法,让我从底层逻辑上完全掌握一道题目。
+
+输入内容: [粘贴 LeetCode 题目描述 或 你看不懂的题解代码]
+
+请按以下结构输出回答:
+
+分:思维图谱(Macro Thinking)
+这道题的核心算法模型是什么?(例如:双指针、分治、滑动窗口等)
+解题的“第一反应”是什么?为什么要选择这种思维方式,而不是其他方式?
+拆:关键动作详解(Micro Breakdown)
+对代码中最容易出错、最关键的几行代码进行逐点详解。
+解释代码中的特殊语法或边界技巧(例如:为什么是 i < j 而不是 i <= j,为什么要设虚拟头节点等)。
+记:深度记忆策略(Cheat Sheet)
+复杂度分析:给出 $O(n)$ 级别的时空复分析,并说明理由。
+记忆口诀:总结出一套 3-5 行的中文顺口溜或步骤简述,帮我快速在大脑中重建逻辑。
+跑:手动模拟一遍(Dry Run)
+给出一个极简的测试数据(例如:数组 [3, 1, 2] 或 链表 1->2),展示代码在每一轮循环中的变量状态变化。
+考:互动挑战(Post-Mortem)
+请问我一个极其尖锐的问题,通常是关于“如果删掉某行代码”或“如果边界值变了”会发生什么,通过我的回答来验证我是否真的学会了。
\ No newline at end of file
diff --git a/HighFrequencyQuestionsSummary.md b/HighFrequencyQuestionsSummary.md
new file mode 100644
index 0000000..bb6af59
--- /dev/null
+++ b/HighFrequencyQuestionsSummary.md
@@ -0,0 +1,1539 @@
+# 高频面试题分类汇总 (总计 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;
+}
+```
diff --git a/anki_binarytree.txt b/anki_binarytree.txt
new file mode 100644
index 0000000..bdb5c93
--- /dev/null
+++ b/anki_binarytree.txt
@@ -0,0 +1,14 @@
+
🌳 102. 二叉树的层序遍历
Medium BFS模板
给你二叉树的根节点 `root` ,返回其节点值的层序遍历(即逐层地,从左到右访问所有节点)。
📎 LeetCode 链接
💡 解题代码
```javascript
// 严格套用 BFS 模板
var levelOrder = function (root) {
if (root === null) return [];
const queue = [root];
const res = [];
while (queue.length > 0) {
// 一次处理一层
const size = queue.length;
const level = [];
for (let i = 0; i < size; i++) {
const node = queue.shift();
level.push(node.val);
if (node.left) queue.push(node.left);
if (node.right) queue.push(node.right);
}
res.push(level);
}
return res;
};
```
+🌳 103. 二叉树的锯齿形层次遍历
Medium
给你二叉树的根节点 `root` ,返回其节点值的锯齿形层序遍历(先从左往右,下一层再从右往左,以此类推,层与层之间交替进行)。
📎 LeetCode 链接
💡 解题代码
```javascript
var zigzagLevelOrder = function(root) {
// 套用 BFS 模板,处理每一层的顺序不同
if (root === null) return [];
const queue = [root];
const res = [];
let isOrderLeft = true; // 标记方向
while (queue.length > 0) {
const size = queue.length;
const level = []; // 使用双端队列思想 (这里简单用数组配合反转)
for (let i = 0; i < size; i++) {
const node = queue.shift();
if (isOrderLeft) {
level.push(node.val);
} else {
level.unshift(node.val); // 倒序就从头插,或者等push完再reverse
}
if (node.left) queue.push(node.left);
if (node.right) queue.push(node.right);
}
res.push(level);
isOrderLeft = !isOrderLeft;
}
return res;
};
```
+🌳 199. 二叉树的右视图
Medium
给定一个二叉树的根节点 `root`,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。
📎 LeetCode 链接
💡 解题代码
```javascript
var rightSideView = function(root) {
// 套用 BFS 模板
if (root === null) return [];
const queue = [root];
const res = [];
while (queue.length > 0) {
const size = queue.length;
// 这一层的最后一个元素,就是右视图看到的元素
for (let i = 0; i < size; i++) {
const node = queue.shift();
// 如果是当前层的最后一个节点,放入结果集
if (i === size - 1) {
res.push(node.val);
}
if (node.left) queue.push(node.left);
if (node.right) queue.push(node.right);
}
}
return res;
};
```
+🌳 662. 二叉树最大宽度
Medium
给定一个二叉树,编写一个函数来获取这个树的最大宽度。每一层的宽度被定义为两个端点之间的长度。
📎 LeetCode 链接
💡 解题代码
```javascript
var widthOfBinaryTree = function(root) {
// 套用 BFS 模板,但需要给节点编号
// 编号规则:root 为 1,左孩子为 2*i,右孩子为 2*i+1
// 关键点:JS数字如果是大数,会精度丢失,需要用 BigInt
if (root === null) return 0;
// queue 存放 [node, index]
// 初始 index 用 1n (BigInt)
const queue = [[root, 1n]];
let maxWidth = 0;
while (queue.length > 0) {
const size = queue.length;
// 记录当前层的 最左索引 和 最右索引
// 肯定分别是队头和队尾(因为是层序的)
// 但注意:for循环执行完后,queue 里剩下的就是下一层的了,所以要在本层开始前记录,或者在循环中记录
let leftIndex, rightIndex;
// 如果想要方便,可以在循环开始前取头尾
// 队头就是本层最左,队尾是本层最右
if (size > 0) {
leftIndex = queue[0][1];
rightIndex = queue[queue.length - 1][1];
// 计算宽度,并转换为 Number
maxWidth = Math.max(maxWidth, Number(rightIndex - leftIndex + 1n));
}
for (let i = 0; i < size; i++) {
const [node, index] = queue.shift();
if (node.left) queue.push([node.left, index * 2n]);
if (node.right) queue.push([node.right, index * 2n + 1n]);
}
}
return maxWidth;
};
```
+ 💡 解题代码
暂无代码答案,请参考 LeetCode 官方题解
+ 💡 解题代码
```javascript
var isSymmetric = function(root) {
if(root === null) return true;
// 递归判断两个子树是否互为镜像
const check = (left, right) => {
// 1. Base Case
if (left === null && right === null) return true; // 都为空,对称
if (left === null || right === null) return false; // 一个空一个不空,不对称
// 2. Divide: 判断
// A. 根节点值是否相同
if (left.val !== right.val) return false;
// B. 递归比较:左子树的左 vs 右子树的右,左子树的右 vs 右子树的左
return check(left.left, right.right) && check(left.right, right.left);
}
return check(root.left, root.right);
};
```
+🌳 105. 从前序与中序遍历序列构造二叉树
Medium
给定两个整数数组 `preorder` 和 `inorder` ,请构造二叉树并返回其根节点。
📎 LeetCode 链接
💡 解题代码
```javascript
var buildTree = function(preorder, inorder) {
// 套用【分治思维】模板
// 1. Base Case: 序列为空,返回 null
if (preorder.length === 0 || inorder.length === 0) {
return null;
}
// 2. 根节点:前序遍历的第一个元素
const rootVal = preorder[0];
const root = new TreeNode(rootVal);
// 3. 找到根节点在中序遍历中的位置,以此划分左右子树
const index = inorder.indexOf(rootVal);
// 4. Divide: 切割数组,递归构建左右子树
// 左子树的中序:[0, index)
// 左子树的前序:[1, index + 1) (长度要和中序一致)
root.left = buildTree(preorder.slice(1, index + 1), inorder.slice(0, index));
// 右子树的中序:[index + 1, end)
// 右子树的前序:[index + 1, end)
root.right = buildTree(preorder.slice(index + 1), inorder.slice(index + 1));
// 5. Return: 返回构建好的根节点
return root;
};
```
+🌳 236. 二叉树的最近公共祖先
Medium 必考
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
📎 LeetCode 链接
💡 解题代码
```javascript
var lowestCommonAncestor = function (root, p, q) {
// 1. Base Case
// 如果是空,或者找到了 p 或 q,直接返回当前节点
if (root === null || root === p || root === q) {
return root;
}
// 2. Divide
const left = lowestCommonAncestor(root.left, p, q);
const right = lowestCommonAncestor(root.right, p, q);
// 3. Conquer
// 如果左右都找到了,说明当前节点是 LCA
if (left !== null && right !== null) {
return root;
}
// 否则返回非空的那个(即找到了 p 或 q 的那一边)
return left !== null ? left : right;
};
```
+🌳 124. 二叉树中的最大路径和
Hard
二叉树中的最大路径和是指路径上节点值的最大和。路径可以是任何节点作为起点和终点。
📎 LeetCode 链接
💡 解题代码
```javascript
var maxPathSum = function (root) {
let maxSum = Number.MIN_SAFE_INTEGER;
// 分治函数:计算以当前节点为根的单边最大路径和
const dfs = (node) => {
// 1. Base Case
if (node === null) {
return 0;
}
// 2. Divide: 计算左右子树的单边最大贡献(负数也不选)
const leftGain = Math.max(dfs(node.left), 0);
const rightGain = Math.max(dfs(node.right), 0);
// 3. Conquer(Update Global Max): 更新全局最大路径和(包含当前节点和左右子树)
const currentPathSum = node.val + leftGain + rightGain;
maxSum = Math.max(maxSum, currentPathSum);
// 4. Return: 返回当前节点的最大单边路径和给父节点
return node.val + Math.max(leftGain, rightGain);
}
dfs(root);
return maxSum;
};
```
+🌳 112. 路径总和
Easy
给你二叉树的根节点 `root` 和一个表示目标和的整数 `targetSum` 。判断该树中是否存在根节点到叶子节点的路径且和等于目标和。
📎 LeetCode 链接
💡 解题代码
暂无代码答案,请参考 LeetCode 官方题解
+🌳 113. 路径总和 II
Medium
给你二叉树的根节点 `root` 和一个整数 `targetSum` ,找出所有从根节点到叶子节点路径总和等于给定目标和的路径。
📎 LeetCode 链接
💡 解题代码
暂无代码答案,请参考 LeetCode 官方题解
+🌳 129. 求根到叶子节点数字之和
Medium
计算从根节点到叶子节点生成的所有数字之和。每条路径代表一个数字(如 $1 \to 2 \to 3$ 代表 $123$)。
📎 LeetCode 链接
💡 解题代码
```javascript
var sumNumbers = function(root) {
// 套用【遍历思维】模板 (Traverse)
// 这里的“全局变量”是 dfs 函数的参数,也可以写在外面
let sum = 0;
const dfs = (node, curNum) => {
// 1. Base Case
if (node === null) return;
// 2. 前序位置:更新当前路径的数字
curNum = curNum * 10 + node.val;
// 3. 判断叶子节点:如果到了叶子,累加结果
if (node.left === null && node.right === null) {
sum += curNum;
return;
}
// 4. 继续遍历左右子树
dfs(node.left, curNum);
dfs(node.right, curNum);
}
dfs(root, 0);
return sum;
};
```
+ 💡 解题代码
```javascript
var isSubStructure = function(A, B) {
// 特殊约定:空树不是子结构
if (!A || !B) return false;
// 1. Base Case: 以当前节点匹配
// 2. Divide: 否则去左子树找,或者去右子树找
return isSame(A, B) || isSubStructure(A.left, B) || isSubStructure(A.right, B);
};
const isSame = (A, B) => {
// 关键点:B 匹配完了,说明找到了,返回 true
if (!B) return true;
// B 没完但 A 完了,说明匹配不上,返回 false
if (!A) return false;
// 值不同,匹配失败
if (A.val !== B.val) return false;
// 必须左右同时匹配
return isSame(A.left, B.left) && isSame(A.right, B.right);
}
```
+🌳 98. 验证二叉搜索树
Medium
给你一个二叉树的根节点 `root` ,判断其是否是一个有效的二叉搜索树。
📎 LeetCode 链接
💡 解题代码
```javascript
var isValidBST = function (root) {
// 利用 BST 性质:中序遍历是有序的
let pre = -Infinity;
// 返回值:是否合法
const dfs = (node) => {
if (node === null) return true;
// 左
if (!dfs(node.left)) return false;
// 根 (检查是否大于前一个值)
if (node.val <= pre) return false;
pre = node.val;
// 右
return dfs(node.right);
}
return dfs(root);
};
```
\ No newline at end of file
diff --git a/anki_linkedlist.csv b/anki_linkedlist.csv
new file mode 100644
index 0000000..26031dd
--- /dev/null
+++ b/anki_linkedlist.csv
@@ -0,0 +1,16 @@
+🔗 206. 反转链表
难度: Easy | 标签: 必背
给你单链表的头节点 `head` ,请你反转链表,并返回反转后的链表。
🔗 LeetCode 链接
var reverseList = function(head) {
let prev = null;
let cur = head;
while (cur !== null) {
let next = cur.next;
cur.next = prev;
prev = cur;
cur = next;
}
return prev;
};
+🔗 92. 反转链表 II
难度: Medium | 标签: 区间反转
给你单链表的头节点 `head` 和两个整数 `left` 和 `right` ,请你反转从位置 `left` 到位置 `right` 的链表节点,返回反转后的链表。
🔗 LeetCode 链接
var reverseBetween = function(head, m, n) {
let dummy = new ListNode(-1);
dummy.next = head;
let pre = dummy;
for (let i = 1; i < m; i++) {
pre = pre.next;
}
let cur = pre.next;
for (let i = 0; i < n - m; i++) {
let next = cur.next;
cur.next = next.next;
next.next = pre.next;
pre.next = next;
}
return dummy.next;
};
+🔗 25. K 个一组翻转链表
难度: Hard | 标签: 面试常客
给你一个链表,每 `k` 个节点一组进行翻转,请你返回翻转后的链表。
🔗 LeetCode 链接
var reverseKGroup = function(head, k) {
let cur = head;
let count = 0;
// 探测是否够 k 个
while (cur !== null && count !== k) {
cur = cur.next;
count++;
}
if (count === k) {
// 反转这 k 个节点
let prev = null;
let node = head;
for (let i = 0; i < k; i++) {
let next = node.next;
node.next = prev;
prev = node;
node = next;
}
// 递归连接
head.next = reverseKGroup(cur, k);
return prev;
}
return head;
};
+🔗 21. 合并两个有序链表
难度: Easy
将两个升序链表合并为一个新的升序链表并返回。
🔗 LeetCode 链接
解法 1:
// 迭代法
var mergeTwoLists = function(l1, l2) {
let dummy = new ListNode(-1);
let pre = dummy;
while (l1 !== null && l2 !== null) {
if (l1.val <= l2.val) {
pre.next = l1;
l1 = l1.next;
} else {
pre.next = l2;
l2 = l2.next;
}
pre = pre.next;
}
pre.next = l1 === null ? l2 : l1;
return dummy.next;
};
解法 2:
//递归
var mergeTwoLists = function(l1, l2) {
if(l1 === null){
return l2;
}else if(l2 === null){
return l1;
}else if(l1.val <= l2.val){
l1.next = mergeTwoLists(l1.next,l2);
return l1;
}else{
l2.next = mergeTwoLists(l1,l2.next);
return l2;
}
};
+🔗 23. 合并K个排序链表
难度: Hard | 标签: 堆/归并
给你一个链表数组,每个链表都已经按升序排列。请你将所有链表合并到一个升序链表中。
🔗 LeetCode 链接
解法 1:
var mergeKLists = function(lists) {
if (lists.length === 0) return null;
return solve(lists, 0, lists.length - 1);
};
function solve(lists, left, right) {
if (left === right) return lists[left];
let mid = Math.floor((left + right) / 2);
let l1 = solve(lists, left, mid);
let l2 = solve(lists, mid + 1, right);
return mergeTwoLists(l1, l2);
}
function mergeTwoLists(l1, l2) {
let dummy = new ListNode(-1);
let pre = dummy;
while (l1 && l2) {
if (l1.val < l2.val) {
pre.next = l1;
l1 = l1.next;
} else {
pre.next = l2;
l2 = l2.next;
}
pre = pre.next;
}
pre.next = l1 || l2;
return dummy.next;
}
解法 2:
var mergeKLists = function(lists) {
let dummy = new ListNode(-1);
let p = dummy;
let pq = new MinHeap((a, b) => a.val < b.val); // 伪代码:假设有最小堆
for (let head of lists) {
if (head) pq.push(head);
}
while (!pq.isEmpty()) {
let node = pq.pop();
p.next = node;
if (node.next) pq.push(node.next);
p = p.next;
}
return dummy.next;
};
+🔗 148. 排序链表
难度: Medium | 标签: 归并排序
给你链表的头结点 `head` ,请将其按升序排列并返回排序后的链表(要求 $O(n \log n)$ 时间复杂度和 $O(1)$ 空间复杂度)。
🔗 LeetCode 链接
var sortList = function(head) {
return mergeSort(head);
};
const mergeSort = head => {
if (head === null || head.next === null) {
return head;
}
let slow = head;
let fast = head.next.next;
while (fast !== null && fast.next !== null) {
slow = slow.next;
fast = fast.next.next;
}
let mid = slow.next;
slow.next = null;
let left = mergeSort(head);
let right = mergeSort(mid);
return merge(left, right);
}
const merge = (l1, l2) => {
let dummy = new ListNode(-1);
let pre = dummy;
while (l1 !== null && l2 !== null) {
if (l1.val < l2.val) {
pre.next = l1;
l1 = l1.next;
} else {
pre.next = l2;
l2 = l2.next;
}
pre = pre.next;
}
pre.next = l1 !== null ? l1 : l2;
return dummy.next;
}
+🔗 补充题1. 排序奇升偶降链表
难度: Medium
给定一个奇数位升序,偶数位降序的链表,将其排序为升序。 (思路:拆分、反转偶数链表、合并)
🔗 LeetCode 链接
暂无代码答案,请参考 LeetCode 官方题解
+🔗 141. 环形链表
难度: Easy | 标签: 判圈
给你一个链表的头节点 `head` ,判断链表中是否有环。
🔗 LeetCode 链接
var hasCycle = function(head) {
let slow = head;
let fast = head;
while (fast !== null && fast.next !== null) {
slow = slow.next;
fast = fast.next.next;
if (slow === fast) return true;
}
return false;
};
+🔗 142. 环形链表 II
难度: Medium | 标签: 找入口
给定一个链表,返回链表开始入环的第一个节点。如果链表无环,则返回 `null`。
🔗 LeetCode 链接
var detectCycle = function(head) {
let slow = head;
let fast = head;
while (fast !== null && fast.next !== null) {
slow = slow.next;
fast = fast.next.next;
if (slow === fast) {
slow = head;
while (slow !== fast) {
slow = slow.next;
fast = fast.next;
}
return slow;
}
}
return null;
};
+🔗 160. 相交链表
难度: Easy
给你两个单链表的头节点 `headA` 和 `headB` ,请你找出并返回两个单链表相交的起始节点。
🔗 LeetCode 链接
var getIntersectionNode = function(headA, headB) {
if (headA === null || headB === null) return null;
let pA = headA;
let pB = headB;
// 只要不相等就继续走
while (pA !== pB) {
// 走完 A 就走 B,走完 B 就走 A
pA = pA === null ? headB : pA.next;
pB = pB === null ? headA : pB.next;
}
return pA;
};
+🔗 19. 删除链表的倒数第N个节点
难度: Medium
给你一个链表,删除链表的倒数第 `n` 个结点,并且返回链表的头结点。
🔗 LeetCode 链接
暂无代码答案,请参考 LeetCode 官方题解
+🔗 剑指 Offer 22. 链表中倒数第k个节点
难度: Easy
输入一个链表,输出该链表中倒数第 `k` 个节点。
🔗 LeetCode 链接
暂无代码答案,请参考 LeetCode 官方题解
+🔗 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$
🔗 LeetCode 链接
var reorderList = function(head) {
if (head === null) return;
let slow = head;
let fast = head;
while (fast !== null && fast.next !== null) {
slow = slow.next;
fast = fast.next.next;
}
let prev = null;
let cur = slow.next;
slow.next = null;
while (cur !== null) {
let next = cur.next;
cur.next = prev;
prev = cur;
cur = next;
}
let head1 = head;
let head2 = prev;
while (head1 !== null && head2 !== null) {
let next1 = head1.next;
let next2 = head2.next;
head1.next = head2;
head1 = next1;
head2.next = next1; // 修正逻辑:head1->head2->next1
head2 = next2;
}
};
+🔗 2. 两数相加
难度: Medium
给你两个非空的链表,表示两个非负的整数。它们每位数字都是按照逆序的方式存储的。请你将两个数相加。
🔗 LeetCode 链接
解法 1:
var genericSolution = function(head) {
let dummy = new ListNode(-1); // 哨兵节点
dummy.next = head;
let pre = dummy;
let cur = head;
while (cur !== null) {
// 逻辑处理...
cur = cur.next;
}
return dummy.next;
};
解法 2:
var findMiddle = function(head) {
let slow = head;
let fast = head.next;
while (fast !== null && fast.next !== null) {
slow = slow.next;
fast = fast.next.next;
}
return slow;
};
解法 3:
var reverseList = function(head) {
let prev = null;
let curr = head;
while (curr !== null) {
let nextTemp = curr.next;
curr.next = prev;
prev = curr;
curr = nextTemp;
}
return prev;
};
+🔗 146. LRU缓存机制
难度: Medium | 标签: 双向链表+哈希
设计并实现一个满足 LRU (最近最少使用) 缓存约束的数据结构。
🔗 LeetCode 链接
/**
* @param {number} capacity
*/
var LRUCache = function(capacity) {
this.capacity = capacity;
this.map = new Map(); // key -> node
this.head = new ListNode(-1, -1); // 虚拟头
this.tail = new ListNode(-1, -1); // 虚拟尾
this.head.next = this.tail;
this.tail.prev = this.head;
};
function ListNode(key, val) {
this.key = key;
this.val = val;
this.prev = null;
this.next = null;
}
/**
* @param {number} key
* @return {number}
*/
LRUCache.prototype.get = function(key) {
if (!this.map.has(key)) return -1;
const node = this.map.get(key);
this.moveToHead(node); // 访问后移到头部
return node.val;
};
/**
* @param {number} key
* @param {number} value
* @return {void}
*/
LRUCache.prototype.put = function(key, value) {
if (this.map.has(key)) {
const node = this.map.get(key);
node.val = value;
this.moveToHead(node);
} else {
if (this.map.size === this.capacity) {
const lastNode = this.tail.prev;
this.removeNode(lastNode);
this.map.delete(lastNode.key);
}
const newNode = new ListNode(key, value);
this.addNodeToHead(newNode);
this.map.set(key, newNode);
}
};
// 辅助函数:将节点移到头部
LRUCache.prototype.moveToHead = function(node) {
this.removeNode(node);
this.addNodeToHead(node);
};
// 辅助函数:删除节点
LRUCache.prototype.removeNode = function(node) {
node.prev.next = node.next;
node.next.prev = node.prev;
};
// 辅助函数:在头部插入节点
LRUCache.prototype.addNodeToHead = function(node) {
node.next = this.head.next;
node.prev = this.head;
this.head.next.prev = node;
this.head.next = node;
};
+🔗 82. 删除排序链表中的重复元素 II
难度: Medium
给定一个已排序的链表的头 `head` ,删除所有含有重复数字的节点,只保留原始链表中未重复出现的数字。
🔗 LeetCode 链接
var deleteDuplicates = function(head) {
let dummy = new ListNode(-1);
dummy.next = head;
let pre = dummy;
while (pre.next !== null && pre.next.next !== null) {
if (pre.next.val === pre.next.next.val) {
let val = pre.next.val;
while (pre.next !== null && pre.next.val === val) {
pre.next = pre.next.next;
}
} else {
pre = pre.next;
}
}
return dummy.next;
}
\ No newline at end of file
diff --git a/anki_linkedlist.txt b/anki_linkedlist.txt
new file mode 100644
index 0000000..5c4c2f2
--- /dev/null
+++ b/anki_linkedlist.txt
@@ -0,0 +1,16 @@
+🔗 206. 反转链表
Easy 必背
给你单链表的头节点 `head` ,请你反转链表,并返回反转后的链表。
📎 LeetCode 链接
💡 解题代码
```javascript
var reverseList = function(head) {
let prev = null;
let cur = head;
while (cur !== null) {
let next = cur.next;
cur.next = prev;
prev = cur;
cur = next;
}
return prev;
};
```
+🔗 92. 反转链表 II
Medium 区间反转
给你单链表的头节点 `head` 和两个整数 `left` 和 `right` ,请你反转从位置 `left` 到位置 `right` 的链表节点,返回反转后的链表。
📎 LeetCode 链接
💡 解题代码
```javascript
var reverseBetween = function(head, m, n) {
let dummy = new ListNode(-1);
dummy.next = head;
let pre = dummy;
for (let i = 1; i < m; i++) {
pre = pre.next;
}
let cur = pre.next;
for (let i = 0; i < n - m; i++) {
let next = cur.next;
cur.next = next.next;
next.next = pre.next;
pre.next = next;
}
return dummy.next;
};
```
+🔗 25. K 个一组翻转链表
Hard 面试常客
给你一个链表,每 `k` 个节点一组进行翻转,请你返回翻转后的链表。
📎 LeetCode 链接
💡 解题代码
```javascript
var reverseKGroup = function(head, k) {
let cur = head;
let count = 0;
// 探测是否够 k 个
while (cur !== null && count !== k) {
cur = cur.next;
count++;
}
if (count === k) {
// 反转这 k 个节点
let prev = null;
let node = head;
for (let i = 0; i < k; i++) {
let next = node.next;
node.next = prev;
prev = node;
node = next;
}
// 递归连接
head.next = reverseKGroup(cur, k);
return prev;
}
return head;
};
```
+ 💡 解题代码
📝 解法 1
```javascript
// 迭代法
var mergeTwoLists = function(l1, l2) {
let dummy = new ListNode(-1);
let pre = dummy;
while (l1 !== null && l2 !== null) {
if (l1.val <= l2.val) {
pre.next = l1;
l1 = l1.next;
} else {
pre.next = l2;
l2 = l2.next;
}
pre = pre.next;
}
pre.next = l1 === null ? l2 : l1;
return dummy.next;
};
```
📝 解法 2
```javascript
//递归
var mergeTwoLists = function(l1, l2) {
if(l1 === null){
return l2;
}else if(l2 === null){
return l1;
}else if(l1.val <= l2.val){
l1.next = mergeTwoLists(l1.next,l2);
return l1;
}else{
l2.next = mergeTwoLists(l1,l2.next);
return l2;
}
};
```
+🔗 23. 合并K个排序链表
Hard 堆/归并
给你一个链表数组,每个链表都已经按升序排列。请你将所有链表合并到一个升序链表中。
📎 LeetCode 链接
💡 解题代码
📝 解法 1
```javascript
var mergeKLists = function(lists) {
if (lists.length === 0) return null;
return solve(lists, 0, lists.length - 1);
};
function solve(lists, left, right) {
if (left === right) return lists[left];
let mid = Math.floor((left + right) / 2);
let l1 = solve(lists, left, mid);
let l2 = solve(lists, mid + 1, right);
return mergeTwoLists(l1, l2);
}
function mergeTwoLists(l1, l2) {
let dummy = new ListNode(-1);
let pre = dummy;
while (l1 && l2) {
if (l1.val < l2.val) {
pre.next = l1;
l1 = l1.next;
} else {
pre.next = l2;
l2 = l2.next;
}
pre = pre.next;
}
pre.next = l1 || l2;
return dummy.next;
}
```
📝 解法 2
```javascript
var mergeKLists = function(lists) {
let dummy = new ListNode(-1);
let p = dummy;
let pq = new MinHeap((a, b) => a.val < b.val); // 伪代码:假设有最小堆
for (let head of lists) {
if (head) pq.push(head);
}
while (!pq.isEmpty()) {
let node = pq.pop();
p.next = node;
if (node.next) pq.push(node.next);
p = p.next;
}
return dummy.next;
};
```
+🔗 148. 排序链表
Medium 归并排序
给你链表的头结点 `head` ,请将其按升序排列并返回排序后的链表(要求 $O(n \log n)$ 时间复杂度和 $O(1)$ 空间复杂度)。
📎 LeetCode 链接
💡 解题代码
```javascript
var sortList = function(head) {
return mergeSort(head);
};
const mergeSort = head => {
if (head === null || head.next === null) {
return head;
}
let slow = head;
let fast = head.next.next;
while (fast !== null && fast.next !== null) {
slow = slow.next;
fast = fast.next.next;
}
let mid = slow.next;
slow.next = null;
let left = mergeSort(head);
let right = mergeSort(mid);
return merge(left, right);
}
const merge = (l1, l2) => {
let dummy = new ListNode(-1);
let pre = dummy;
while (l1 !== null && l2 !== null) {
if (l1.val < l2.val) {
pre.next = l1;
l1 = l1.next;
} else {
pre.next = l2;
l2 = l2.next;
}
pre = pre.next;
}
pre.next = l1 !== null ? l1 : l2;
return dummy.next;
}
```
+🔗 补充题1. 排序奇升偶降链表
Medium
给定一个奇数位升序,偶数位降序的链表,将其排序为升序。 (思路:拆分、反转偶数链表、合并)
📎 LeetCode 链接
💡 解题代码
暂无代码答案,请参考 LeetCode 官方题解
+ 💡 解题代码
```javascript
var hasCycle = function(head) {
let slow = head;
let fast = head;
while (fast !== null && fast.next !== null) {
slow = slow.next;
fast = fast.next.next;
if (slow === fast) return true;
}
return false;
};
```
+🔗 142. 环形链表 II
Medium 找入口
给定一个链表,返回链表开始入环的第一个节点。如果链表无环,则返回 `null`。
📎 LeetCode 链接
💡 解题代码
```javascript
var detectCycle = function(head) {
let slow = head;
let fast = head;
while (fast !== null && fast.next !== null) {
slow = slow.next;
fast = fast.next.next;
if (slow === fast) {
slow = head;
while (slow !== fast) {
slow = slow.next;
fast = fast.next;
}
return slow;
}
}
return null;
};
```
+🔗 160. 相交链表
Easy
给你两个单链表的头节点 `headA` 和 `headB` ,请你找出并返回两个单链表相交的起始节点。
📎 LeetCode 链接
💡 解题代码
```javascript
var getIntersectionNode = function(headA, headB) {
if (headA === null || headB === null) return null;
let pA = headA;
let pB = headB;
// 只要不相等就继续走
while (pA !== pB) {
// 走完 A 就走 B,走完 B 就走 A
pA = pA === null ? headB : pA.next;
pB = pB === null ? headA : pB.next;
}
return pA;
};
```
+🔗 19. 删除链表的倒数第N个节点
Medium
给你一个链表,删除链表的倒数第 `n` 个结点,并且返回链表的头结点。
📎 LeetCode 链接
💡 解题代码
暂无代码答案,请参考 LeetCode 官方题解
+🔗 剑指 Offer 22. 链表中倒数第k个节点
Easy
输入一个链表,输出该链表中倒数第 `k` 个节点。
📎 LeetCode 链接
💡 解题代码
暂无代码答案,请参考 LeetCode 官方题解
+🔗 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$
📎 LeetCode 链接
💡 解题代码
```javascript
var reorderList = function(head) {
if (head === null) return;
let slow = head;
let fast = head;
while (fast !== null && fast.next !== null) {
slow = slow.next;
fast = fast.next.next;
}
let prev = null;
let cur = slow.next;
slow.next = null;
while (cur !== null) {
let next = cur.next;
cur.next = prev;
prev = cur;
cur = next;
}
let head1 = head;
let head2 = prev;
while (head1 !== null && head2 !== null) {
let next1 = head1.next;
let next2 = head2.next;
head1.next = head2;
head1 = next1;
head2.next = next1; // 修正逻辑:head1->head2->next1
head2 = next2;
}
};
```
+🔗 2. 两数相加
Medium
给你两个非空的链表,表示两个非负的整数。它们每位数字都是按照逆序的方式存储的。请你将两个数相加。
📎 LeetCode 链接
💡 解题代码
📝 解法 1
```javascript
var genericSolution = function(head) {
let dummy = new ListNode(-1); // 哨兵节点
dummy.next = head;
let pre = dummy;
let cur = head;
while (cur !== null) {
// 逻辑处理...
cur = cur.next;
}
return dummy.next;
};
```
📝 解法 2
```javascript
var findMiddle = function(head) {
let slow = head;
let fast = head.next;
while (fast !== null && fast.next !== null) {
slow = slow.next;
fast = fast.next.next;
}
return slow;
};
```
📝 解法 3
```javascript
var reverseList = function(head) {
let prev = null;
let curr = head;
while (curr !== null) {
let nextTemp = curr.next;
curr.next = prev;
prev = curr;
curr = nextTemp;
}
return prev;
};
```
+🔗 146. LRU缓存机制
Medium 双向链表+哈希
设计并实现一个满足 LRU (最近最少使用) 缓存约束的数据结构。
📎 LeetCode 链接
💡 解题代码
```javascript
/**
* @param {number} capacity
*/
var LRUCache = function(capacity) {
this.capacity = capacity;
this.map = new Map(); // key -> node
this.head = new ListNode(-1, -1); // 虚拟头
this.tail = new ListNode(-1, -1); // 虚拟尾
this.head.next = this.tail;
this.tail.prev = this.head;
};
function ListNode(key, val) {
this.key = key;
this.val = val;
this.prev = null;
this.next = null;
}
/**
* @param {number} key
* @return {number}
*/
LRUCache.prototype.get = function(key) {
if (!this.map.has(key)) return -1;
const node = this.map.get(key);
this.moveToHead(node); // 访问后移到头部
return node.val;
};
/**
* @param {number} key
* @param {number} value
* @return {void}
*/
LRUCache.prototype.put = function(key, value) {
if (this.map.has(key)) {
const node = this.map.get(key);
node.val = value;
this.moveToHead(node);
} else {
if (this.map.size === this.capacity) {
const lastNode = this.tail.prev;
this.removeNode(lastNode);
this.map.delete(lastNode.key);
}
const newNode = new ListNode(key, value);
this.addNodeToHead(newNode);
this.map.set(key, newNode);
}
};
// 辅助函数:将节点移到头部
LRUCache.prototype.moveToHead = function(node) {
this.removeNode(node);
this.addNodeToHead(node);
};
// 辅助函数:删除节点
LRUCache.prototype.removeNode = function(node) {
node.prev.next = node.next;
node.next.prev = node.prev;
};
// 辅助函数:在头部插入节点
LRUCache.prototype.addNodeToHead = function(node) {
node.next = this.head.next;
node.prev = this.head;
this.head.next.prev = node;
this.head.next = node;
};
```
+🔗 82. 删除排序链表中的重复元素 II
Medium
给定一个已排序的链表的头 `head` ,删除所有含有重复数字的节点,只保留原始链表中未重复出现的数字。
📎 LeetCode 链接
💡 解题代码
```javascript
var deleteDuplicates = function(head) {
let dummy = new ListNode(-1);
dummy.next = head;
let pre = dummy;
while (pre.next !== null && pre.next.next !== null) {
if (pre.next.val === pre.next.next.val) {
let val = pre.next.val;
while (pre.next !== null && pre.next.val === val) {
pre.next = pre.next.next;
}
} else {
pre = pre.next;
}
}
return dummy.next;
}
```
\ No newline at end of file
diff --git a/generate_anki.js b/generate_anki.js
new file mode 100644
index 0000000..a323ab7
--- /dev/null
+++ b/generate_anki.js
@@ -0,0 +1,287 @@
+const fs = require('fs');
+const path = require('path');
+
+const summaryFile = path.resolve('HighFrequencyQuestionsSummary.md');
+const linkedListFile = path.resolve('数据结构篇/链表.md');
+const outputFile = path.resolve('anki_linkedlist.txt');
+
+function escapeForAnki(text) {
+ if (!text) return '';
+ return text
+ .replace(/\r\n/g, ' ')
+ .replace(/\n/g, ' ')
+ .replace(/\r/g, ' ')
+ .replace(/\t/g, ' ');
+}
+
+/**
+ * 简单的 JavaScript 语法高亮
+ */
+function highlightJS(code) {
+ // 先转义 HTML 特殊字符
+ let html = code
+ .replace(/&/g, '&')
+ .replace(//g, '>');
+
+ // 关键字 - 紫色
+ const keywords = ['const', 'let', 'var', 'function', 'return', 'if', 'else', 'while', 'for', 'break', 'continue', 'new', 'this', 'null', 'true', 'false', 'typeof', 'instanceof', 'class', 'extends', 'constructor', 'static', 'get', 'set', 'async', 'await', 'try', 'catch', 'throw', 'finally', 'of', 'in'];
+
+ // 内置对象/方法 - 青色
+ const builtins = ['Math', 'Array', 'Object', 'String', 'Number', 'Boolean', 'Map', 'Set', 'console', 'prototype', 'length', 'push', 'pop', 'shift', 'unshift', 'slice', 'splice', 'concat', 'join', 'reverse', 'sort', 'filter', 'map', 'reduce', 'forEach', 'find', 'findIndex', 'includes', 'indexOf', 'fill', 'from', 'floor', 'ceil', 'max', 'min', 'abs', 'log', 'pow', 'sqrt', 'random', 'hasOwnProperty', 'toString', 'valueOf', 'substring', 'substr', 'split', 'trim', 'replace', 'match', 'test', 'exec', 'has', 'get', 'set', 'delete', 'add', 'clear', 'keys', 'values', 'entries', 'size', 'isEmpty'];
+
+ // 注释 - 灰绿色 (先处理,避免被其他规则影响)
+ html = html.replace(/(\/\/[^\n]*)/g, '$1');
+ html = html.replace(/(\/\*[\s\S]*?\*\/)/g, '$1');
+
+ // 字符串 - 橙色
+ html = html.replace(/("[^&]*?"|'[^']*?'|`[^`]*?`)/g, '$1');
+
+ // 数字 - 浅绿色
+ html = html.replace(/\b(\d+\.?\d*)\b/g, '$1');
+
+ // 关键字高亮 - 紫色
+ for (const kw of keywords) {
+ const regex = new RegExp(`\\b(${kw})\\b`, 'g');
+ html = html.replace(regex, '$1');
+ }
+
+ // 内置对象高亮 - 青色
+ for (const builtin of builtins) {
+ const regex = new RegExp(`\\b(${builtin})\\b`, 'g');
+ html = html.replace(regex, '$1');
+ }
+
+ // 函数名 - 黄色 (函数定义和调用)
+ html = html.replace(/\b([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/g, '$1(');
+
+ // 箭头函数
+ html = html.replace(/=>/g, '=>');
+
+ return html;
+}
+
+function formatCodeForAnki(code) {
+ if (!code) return '';
+
+ // 应用语法高亮
+ const highlighted = highlightJS(code.trim());
+
+ // 换行转
+ const withBreaks = highlighted
+ .replace(/\r\n/g, '
')
+ .replace(/\n/g, '
')
+ .replace(/\r/g, '
')
+ .replace(/\t/g, ' ');
+
+ // 返回带样式的代码块
+ return `\`\`\`javascript
${withBreaks}
\`\`\`
`;
+}
+
+function parseSummaryLinkedList(content) {
+ const questions = [];
+ const lines = content.split('\n');
+
+ let inLinkedListSection = false;
+ let currentCategory = '';
+
+ for (let i = 0; i < lines.length; i++) {
+ const line = lines[i];
+
+ if (line.includes('## 一、 链表')) {
+ inLinkedListSection = true;
+ continue;
+ }
+
+ if (inLinkedListSection && line.match(/^## [二三四五六七八九十]/)) {
+ break;
+ }
+
+ if (!inLinkedListSection) continue;
+
+ const categoryMatch = line.match(/^\*\*(.+)\*\*$/);
+ if (categoryMatch) {
+ currentCategory = categoryMatch[1];
+ continue;
+ }
+
+ const questionMatch = line.match(/^- \[(.+?)\]\((.+?)\)\s*\((\w+)\)(?:\s*-\s*\*(.+?)\*)?/);
+ if (questionMatch) {
+ const title = questionMatch[1];
+ const link = questionMatch[2];
+ const difficulty = questionMatch[3];
+ const tag = questionMatch[4] || '';
+
+ let description = '';
+ if (i + 1 < lines.length && lines[i + 1].trim().startsWith('>')) {
+ description = lines[i + 1].trim().replace(/^>\s*/, '');
+ }
+
+ questions.push({ title, link, difficulty, tag, category: currentCategory, description });
+ }
+
+ const supplementMatch = line.match(/^- \[?(补充题\d*[..]\s*.+?)\]?\s*\((.+?)\)\s*\((\w+)\)?/);
+ if (supplementMatch && !questionMatch) {
+ const title = supplementMatch[1];
+ const link = supplementMatch[2];
+ const difficulty = supplementMatch[3] || 'Medium';
+
+ let description = '';
+ if (i + 1 < lines.length && lines[i + 1].trim().startsWith('>')) {
+ description = lines[i + 1].trim().replace(/^>\s*/, '');
+ }
+
+ questions.push({ title, link, difficulty, tag: '', category: currentCategory, description });
+ }
+ }
+
+ return questions;
+}
+
+function parseLinkedListSolutions(content) {
+ const solutions = {};
+ const lines = content.split('\n');
+
+ let currentQuestionNum = null;
+ let currentCode = '';
+ let inCodeBlock = false;
+
+ for (let i = 0; i < lines.length; i++) {
+ const line = lines[i];
+
+ const titleMatch = line.match(/^#{3,5}\s*\[?(\d+)[..\s]|\b(\d+)[..]/);
+ if (titleMatch) {
+ const num = titleMatch[1] || titleMatch[2];
+ if (num) {
+ currentQuestionNum = num;
+ if (!solutions[currentQuestionNum]) {
+ solutions[currentQuestionNum] = [];
+ }
+ }
+ }
+
+ if (line.trim().startsWith('```js') || line.trim().startsWith('```javascript')) {
+ inCodeBlock = true;
+ currentCode = '';
+ continue;
+ }
+
+ if (line.trim() === '```' && inCodeBlock) {
+ inCodeBlock = false;
+ if (currentQuestionNum && currentCode.trim()) {
+ solutions[currentQuestionNum].push(currentCode.trim());
+ }
+ currentCode = '';
+ continue;
+ }
+
+ if (inCodeBlock) {
+ currentCode += line + '\n';
+ }
+ }
+
+ return solutions;
+}
+
+function generateAnkiCards(questions, solutions) {
+ const cards = [];
+
+ for (const q of questions) {
+ const numMatch = q.title.match(/^(\d+)/);
+ const questionNum = numMatch ? numMatch[1] : null;
+
+ // 正面 - 题目卡片
+ let front = ``;
+ front += `
🔗 ${escapeForAnki(q.title)}
`;
+
+ const diffColor = q.difficulty === 'Easy' ? '#4CAF50' : q.difficulty === 'Medium' ? '#FF9800' : '#f44336';
+ const diffBg = q.difficulty === 'Easy' ? '#E8F5E9' : q.difficulty === 'Medium' ? '#FFF3E0' : '#FFEBEE';
+ front += `
${q.difficulty}`;
+ if (q.tag) front += ` ${escapeForAnki(q.tag)}`;
+ if (q.category) front += ` ${escapeForAnki(q.category)}`;
+ front += `
`;
+
+ if (q.description) {
+ front += `
${escapeForAnki(q.description)}
`;
+ }
+ front += `
📎 LeetCode 链接
`;
+ front += `
`;
+
+ // 反面 - 答案
+ let back = ``;
+ back += `
💡 解题代码
`;
+
+ if (questionNum && solutions[questionNum] && solutions[questionNum].length > 0) {
+ const codes = solutions[questionNum];
+ codes.forEach((code, idx) => {
+ if (codes.length > 1) {
+ back += `
📝 解法 ${idx + 1}
`;
+ }
+ back += formatCodeForAnki(code);
+ if (idx < codes.length - 1) back += `
`;
+ });
+ } else {
+ back += `
暂无代码答案,请参考 LeetCode 官方题解
`;
+ }
+ back += `
`;
+
+ cards.push({ front, back, title: q.title });
+ }
+
+ return cards;
+}
+
+function writeAnkiFile(cards, outputPath) {
+ const lines = cards.map(card => {
+ const front = card.front.replace(/[\r\n]+/g, ' ').replace(/\t/g, ' ');
+ const back = card.back.replace(/[\r\n]+/g, ' ').replace(/\t/g, ' ');
+ return `${front}\t${back}`;
+ });
+
+ fs.writeFileSync(outputPath, lines.join('\n'), 'utf8');
+}
+
+// 主程序
+try {
+ console.log('📖 读取摘要文件...');
+ const summaryContent = fs.readFileSync(summaryFile, 'utf8');
+
+ console.log('📖 读取链表详解文件...');
+ const linkedListContent = fs.readFileSync(linkedListFile, 'utf8');
+
+ console.log('🔍 解析链表题目...');
+ const questions = parseSummaryLinkedList(summaryContent);
+ console.log(` 找到 ${questions.length} 道链表题目`);
+
+ console.log('🔍 解析代码答案...');
+ const solutions = parseLinkedListSolutions(linkedListContent);
+ console.log(` 找到 ${Object.keys(solutions).length} 个题目的代码答案`);
+
+ console.log('🎨 生成带语法高亮的 Anki 卡片...');
+ const cards = generateAnkiCards(questions, solutions);
+
+ console.log('💾 写入文件...');
+ writeAnkiFile(cards, outputFile);
+
+ console.log(`\n✅ 成功生成 ${cards.length} 张 Anki 卡片(带语法高亮)!`);
+ console.log(`📁 输出文件: ${outputFile}`);
+ console.log('\n📋 Anki 导入说明:');
+ console.log(' 1. 打开 Anki -> 文件 -> 导入');
+ console.log(' 2. 选择 anki_linkedlist.txt');
+ console.log(' 3. 类型: 基础');
+ console.log(' 4. 字段分隔符: Tab');
+ console.log(' 5. ✅ 务必勾选 "允许在字段中使用 HTML"');
+ console.log(' 6. 字段1 -> 正面, 字段2 -> 反面');
+
+ console.log('\n📊 卡片详情:');
+ let matchedCount = 0;
+ for (const card of cards) {
+ const hasAnswer = !card.back.includes('暂无代码答案');
+ if (hasAnswer) matchedCount++;
+ console.log(` ${hasAnswer ? '✅' : '❌'} ${card.title}`);
+ }
+ console.log(`\n 匹配率: ${matchedCount}/${cards.length} (${Math.round(matchedCount / cards.length * 100)}%)`);
+
+} catch (err) {
+ console.error('❌ 错误:', err.message);
+}
diff --git a/generate_anki_binarytree.js b/generate_anki_binarytree.js
new file mode 100644
index 0000000..8523fd9
--- /dev/null
+++ b/generate_anki_binarytree.js
@@ -0,0 +1,282 @@
+const fs = require('fs');
+const path = require('path');
+
+const summaryFile = path.resolve('HighFrequencyQuestionsSummary.md');
+const binaryTreeFile = path.resolve('数据结构篇/二叉树.md');
+const outputFile = path.resolve('anki_binarytree.txt');
+
+function escapeForAnki(text) {
+ if (!text) return '';
+ return text
+ .replace(/\r\n/g, ' ')
+ .replace(/\n/g, ' ')
+ .replace(/\r/g, ' ')
+ .replace(/\t/g, ' ');
+}
+
+/**
+ * 简单的 JavaScript 语法高亮
+ */
+function highlightJS(code) {
+ let html = code
+ .replace(/&/g, '&')
+ .replace(//g, '>');
+
+ const keywords = ['const', 'let', 'var', 'function', 'return', 'if', 'else', 'while', 'for', 'break', 'continue', 'new', 'this', 'null', 'true', 'false', 'typeof', 'instanceof', 'class', 'extends', 'constructor', 'static', 'get', 'set', 'async', 'await', 'try', 'catch', 'throw', 'finally', 'of', 'in'];
+ const builtins = ['Math', 'Array', 'Object', 'String', 'Number', 'Boolean', 'Map', 'Set', 'console', 'prototype', 'length', 'push', 'pop', 'shift', 'unshift', 'slice', 'splice', 'concat', 'join', 'reverse', 'sort', 'filter', 'map', 'reduce', 'forEach', 'find', 'findIndex', 'includes', 'indexOf', 'fill', 'from', 'floor', 'ceil', 'max', 'min', 'abs', 'log', 'pow', 'sqrt', 'random', 'hasOwnProperty', 'toString', 'valueOf', 'substring', 'substr', 'split', 'trim', 'replace', 'match', 'test', 'exec', 'has', 'get', 'set', 'delete', 'add', 'clear', 'keys', 'values', 'entries', 'size', 'isEmpty', 'TreeNode', 'ListNode'];
+
+ html = html.replace(/(\/\/[^\n]*)/g, '$1');
+ html = html.replace(/(\/\*[\s\S]*?\*\/)/g, '$1');
+ html = html.replace(/("[^&]*?"|'[^']*?'|`[^`]*?`)/g, '$1');
+ html = html.replace(/\b(\d+\.?\d*)\b/g, '$1');
+
+ for (const kw of keywords) {
+ const regex = new RegExp(`\\b(${kw})\\b`, 'g');
+ html = html.replace(regex, '$1');
+ }
+
+ for (const builtin of builtins) {
+ const regex = new RegExp(`\\b(${builtin})\\b`, 'g');
+ html = html.replace(regex, '$1');
+ }
+
+ html = html.replace(/\b([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/g, '$1(');
+ html = html.replace(/=>/g, '=>');
+
+ return html;
+}
+
+function formatCodeForAnki(code) {
+ if (!code) return '';
+
+ const highlighted = highlightJS(code.trim());
+ const withBreaks = highlighted
+ .replace(/\r\n/g, '
')
+ .replace(/\n/g, '
')
+ .replace(/\r/g, '
')
+ .replace(/\t/g, ' ');
+
+ return `\`\`\`javascript
${withBreaks}
\`\`\`
`;
+}
+
+/**
+ * 从摘要文件中解析二叉树章节的题目
+ */
+function parseSummaryBinaryTree(content) {
+ const questions = [];
+ const lines = content.split('\n');
+
+ let inBinaryTreeSection = false;
+ let currentCategory = '';
+
+ for (let i = 0; i < lines.length; i++) {
+ const line = lines[i];
+
+ // 检测二叉树章节开始
+ if (line.includes('## 二、 二叉树')) {
+ inBinaryTreeSection = true;
+ continue;
+ }
+
+ // 检测二叉树章节结束(下一个二级标题)
+ if (inBinaryTreeSection && line.match(/^## [三四五六七八九十]/)) {
+ break;
+ }
+
+ if (!inBinaryTreeSection) continue;
+
+ const categoryMatch = line.match(/^\*\*(.+)\*\*$/);
+ if (categoryMatch) {
+ currentCategory = categoryMatch[1];
+ continue;
+ }
+
+ // 解析题目行
+ const questionMatch = line.match(/^- \[(.+?)\]\((.+?)\)\s*\((\w+)\)(?:\s*-\s*\*(.+?)\*)?/);
+ if (questionMatch) {
+ const title = questionMatch[1];
+ const link = questionMatch[2];
+ const difficulty = questionMatch[3];
+ const tag = questionMatch[4] || '';
+
+ let description = '';
+ if (i + 1 < lines.length && lines[i + 1].trim().startsWith('>')) {
+ description = lines[i + 1].trim().replace(/^>\s*/, '');
+ }
+
+ questions.push({ title, link, difficulty, tag, category: currentCategory, description });
+ }
+ }
+
+ return questions;
+}
+
+/**
+ * 从详细文件中提取题目对应的代码答案
+ */
+function parseBinaryTreeSolutions(content) {
+ const solutions = {};
+ const lines = content.split('\n');
+
+ let currentQuestionNum = null;
+ let currentCode = '';
+ let inCodeBlock = false;
+
+ for (let i = 0; i < lines.length; i++) {
+ const line = lines[i];
+
+ // 检测题目标题 - 多种格式
+ // [102. 二叉树的层序遍历]
+ // [236. 二叉树的最近公共祖先]
+ // [LCR 143. 树的子结构] -> 对应剑指 Offer 26
+ const titleMatch = line.match(/\[(\d+)[..\s]/);
+ if (titleMatch) {
+ const num = titleMatch[1];
+ if (num) {
+ currentQuestionNum = num;
+ if (!solutions[currentQuestionNum]) {
+ solutions[currentQuestionNum] = [];
+ }
+ }
+ }
+
+ // 特殊处理剑指 Offer 题目
+ if (line.includes('LCR 143') || line.includes('树的子结构')) {
+ currentQuestionNum = '26'; // 剑指 Offer 26
+ if (!solutions[currentQuestionNum]) {
+ solutions[currentQuestionNum] = [];
+ }
+ }
+
+ if (line.trim().startsWith('```js') || line.trim().startsWith('```javascript')) {
+ inCodeBlock = true;
+ currentCode = '';
+ continue;
+ }
+
+ if (line.trim() === '```' && inCodeBlock) {
+ inCodeBlock = false;
+ if (currentQuestionNum && currentCode.trim()) {
+ solutions[currentQuestionNum].push(currentCode.trim());
+ }
+ currentCode = '';
+ continue;
+ }
+
+ if (inCodeBlock) {
+ currentCode += line + '\n';
+ }
+ }
+
+ return solutions;
+}
+
+function generateAnkiCards(questions, solutions) {
+ const cards = [];
+
+ for (const q of questions) {
+ // 从题目标题中提取题号
+ const numMatch = q.title.match(/^(\d+)/);
+ let questionNum = numMatch ? numMatch[1] : null;
+
+ // 特殊处理剑指 Offer 题目
+ if (q.title.includes('剑指 Offer 26') || q.title.includes('树的子结构')) {
+ questionNum = '26';
+ }
+
+ // 正面 - 题目卡片 (绿色主题,适合二叉树)
+ let front = ``;
+ front += `
🌳 ${escapeForAnki(q.title)}
`;
+
+ const diffColor = q.difficulty === 'Easy' ? '#4CAF50' : q.difficulty === 'Medium' ? '#FF9800' : '#f44336';
+ const diffBg = q.difficulty === 'Easy' ? '#E8F5E9' : q.difficulty === 'Medium' ? '#FFF3E0' : '#FFEBEE';
+ front += `
${q.difficulty}`;
+ if (q.tag) front += ` ${escapeForAnki(q.tag)}`;
+ if (q.category) front += ` ${escapeForAnki(q.category)}`;
+ front += `
`;
+
+ if (q.description) {
+ front += `
${escapeForAnki(q.description)}
`;
+ }
+ front += `
📎 LeetCode 链接
`;
+ front += `
`;
+
+ // 反面 - 答案
+ let back = ``;
+ back += `
💡 解题代码
`;
+
+ if (questionNum && solutions[questionNum] && solutions[questionNum].length > 0) {
+ const codes = solutions[questionNum];
+ codes.forEach((code, idx) => {
+ if (codes.length > 1) {
+ back += `
📝 解法 ${idx + 1}
`;
+ }
+ back += formatCodeForAnki(code);
+ if (idx < codes.length - 1) back += `
`;
+ });
+ } else {
+ back += `
暂无代码答案,请参考 LeetCode 官方题解
`;
+ }
+ back += `
`;
+
+ cards.push({ front, back, title: q.title });
+ }
+
+ return cards;
+}
+
+function writeAnkiFile(cards, outputPath) {
+ const lines = cards.map(card => {
+ const front = card.front.replace(/[\r\n]+/g, ' ').replace(/\t/g, ' ');
+ const back = card.back.replace(/[\r\n]+/g, ' ').replace(/\t/g, ' ');
+ return `${front}\t${back}`;
+ });
+
+ fs.writeFileSync(outputPath, lines.join('\n'), 'utf8');
+}
+
+// 主程序
+try {
+ console.log('📖 读取摘要文件...');
+ const summaryContent = fs.readFileSync(summaryFile, 'utf8');
+
+ console.log('📖 读取二叉树详解文件...');
+ const binaryTreeContent = fs.readFileSync(binaryTreeFile, 'utf8');
+
+ console.log('🔍 解析二叉树题目...');
+ const questions = parseSummaryBinaryTree(summaryContent);
+ console.log(` 找到 ${questions.length} 道二叉树题目`);
+
+ console.log('🔍 解析代码答案...');
+ const solutions = parseBinaryTreeSolutions(binaryTreeContent);
+ console.log(` 找到 ${Object.keys(solutions).length} 个题目的代码答案`);
+
+ console.log('🎨 生成带语法高亮的 Anki 卡片...');
+ const cards = generateAnkiCards(questions, solutions);
+
+ console.log('💾 写入文件...');
+ writeAnkiFile(cards, outputFile);
+
+ console.log(`\n✅ 成功生成 ${cards.length} 张二叉树 Anki 卡片!`);
+ console.log(`📁 输出文件: ${outputFile}`);
+ console.log('\n📋 Anki 导入说明:');
+ console.log(' 1. 打开 Anki -> 文件 -> 导入');
+ console.log(' 2. 选择 anki_binarytree.txt');
+ console.log(' 3. 类型: 基础');
+ console.log(' 4. 字段分隔符: Tab');
+ console.log(' 5. ✅ 务必勾选 "允许在字段中使用 HTML"');
+ console.log(' 6. 字段1 -> 正面, 字段2 -> 反面');
+
+ console.log('\n📊 卡片详情:');
+ let matchedCount = 0;
+ for (const card of cards) {
+ const hasAnswer = !card.back.includes('暂无代码答案');
+ if (hasAnswer) matchedCount++;
+ console.log(` ${hasAnswer ? '✅' : '❌'} ${card.title}`);
+ }
+ console.log(`\n 匹配率: ${matchedCount}/${cards.length} (${Math.round(matchedCount / cards.length * 100)}%)`);
+
+} catch (err) {
+ console.error('❌ 错误:', err.message);
+}
diff --git a/test.js b/test.js
new file mode 100644
index 0000000..6ab9106
--- /dev/null
+++ b/test.js
@@ -0,0 +1,573 @@
+var reverse = (head) => {
+ let pre = null;
+ let cur = head;
+ while (cur !== null) {
+ let next = cur.next;
+ cur.next = pre;
+ pre = cur;
+ cur = next;
+ }
+ return pre;
+}
+
+var reverse2 = (head, left, right) => {
+ let dummy = new ListNode(-1);
+ dummy.next = head;
+ let pre = dummy;
+
+ for (let i = 1; i < left; i++) {
+ pre = pre.next;
+ }
+ let cur = pre.next;
+ for (let i = 0; i < right - left; i++) {
+ let next = cur.next;
+ cur.next = next.next;
+ next.next = pre.next;
+ pre.next = next;
+ }
+ return dummy.next;
+}
+
+var kGroup = (head, k) => {
+ let cur = head;
+ let count = 0;
+ while (cur !== null && count !== k) {
+ count++;
+ cur = cur.next;
+ }
+
+ if (k === count) {
+ let pre = null;
+ let node = cur;
+ while (node !== null) {
+ let next = node.next;
+ node.next = pre;
+ pre = node;
+ node = next;
+ }
+ head.next = kGroup(cur, k);
+ return pre;
+ }
+ return head;
+}
+
+var reverse22 = (head, left, right) => {
+ let dummy = new ListNode(-1);
+ dummy.next = head;
+ let pre = head;
+ for (let i = 1; i < left; i++) {
+ pre = pre.next;
+ }
+ let cur = pre.next;
+ for (let i = 0; i < right - left; i++) {
+ let next = cur.next;
+ cur.next = next.next;
+ next.next = pre.next;
+ pre.next = next;
+ }
+ return dummy.next;
+}
+
+var kGroup2 = (head, k) => {
+ let cur = head;
+ let count = 0;
+ while (cur !== null && count !== k) {
+ cur = cur.next;
+ count++;
+ }
+ if (k === count) {
+ let pre = null;
+ let node = head;
+ for (let i = 0; i < k; i++) {
+ let next = node.next;
+ node.next = pre;
+ pre = node;
+ node = next;
+ }
+ head.next = kGroup2(cur, k);
+ return pre;
+ }
+ return head;
+}
+
+var mergeTwoList = (l1, l2) => {
+ let p1 = l1;
+ let p2 = l2;
+ let dummy = new ListNode(-1);
+ let p = dummy;
+ while (l1 && l2) {
+ if (p1.val <= p2.val) {
+ p.next = p1;
+ p1 = p1.next;
+ } else {
+ p.next = p2;
+ p2 = p2.next;
+ }
+ p = p.next;
+ }
+ p.next = l1 === null ? l2 : l1;
+ return dummy.next;
+}
+
+var reverse222 = (head, left, right) => {
+ let dummy = new ListNode(-1);
+ dummy.next = head;
+ let pre = head;
+ for (let i = 1; i < left; i++) {
+ pre = pre.next;
+ }
+ let cur = pre.next;
+ for (let i = 0; i < right - left; i++) {
+ let next = cur.next;
+ cur.next = next.next;
+ next.next = pre.next;
+ pre.next = next;
+ }
+ return dummy.next;
+}
+
+var kGroup3 = (head, k) => {
+ let cur = head;
+ let count = 0;
+
+ while (cur !== null && count !== k) {
+ cur = cur.next;
+ count++;
+ }
+
+ if (k === count) {
+ let pre = null;
+ let node = head;
+ for (let i = 0; i < k; i++) {
+ let next = node.next;
+ node.next = pre;
+ pre = node;
+ node = next;
+ }
+ head.next = kGroup3(cur, k);
+ return pre;
+ }
+ return head;
+}
+
+var mergeTwo = (l1, l2) => {
+ let dummy = new ListNode(-1);
+ let pre = dummy;
+ while(l1 && l2){
+ if(l1.val <= l2.val){
+ pre.next = l1;
+ l1 = l1.next;
+ }else{
+ pre.next = l2;
+ l2 = l2.next;
+ }
+ pre = pre.next;
+ }
+ pre.next = l1 === null ? l2 : l1;
+ return dummy.next;
+}
+
+var reversek = (head, k) => {
+ let count = 0;
+ let cur = head;
+ while(cur !== null && k !== count){
+ cur = cur.next;
+ count++;
+ }
+ if(count === k){
+ let pre = null;
+ let node = head;
+ for(let i = 0; i < k; i++){
+ let next = node.next;
+ node.next = next.next;
+ next.next = pre.next;
+ pre.next = next;
+ }
+ head.next = reversek(cur, k);
+ return pre;
+ }
+ return head;
+}
+
+var mergeKList = (lists) => {
+ if(lists.length === 0) return null;
+ return solve(lists, 0, lists.length);
+}
+
+function solve(lists, left, right){
+ if(left === right) return lists[left];
+ let mid = Math.floor((left + right) / 2);
+ let l1 = solve(lists, left, mid);
+ let l2 = solve(lists, mid + 1, right);
+ return mergeTwoList1(l1, l2);
+}
+
+function mergeTwoList1(l1, l2){
+ let dummy = new ListNode(-1);
+ let pre = dummy;
+ while(l1 && l2){
+ if(l1.val < l2.val){
+ pre.next = l1;
+ l1 = l1.next;
+ }else{
+ pre.next = l2;
+ l2 = l2.next;
+ }
+ pre = pre.next;
+ }
+ pre.next = l1 === null ? l2 : l1;
+ return dummy.next;
+}
+
+var reverse2222 = (head, left, right) => {
+ let dummy = new ListNode(-1);
+ dummy.next = head;
+ let pre = dummy;
+ for(let i = 1; i < left; i++){
+ pre = pre.next;
+ }
+ let cur = pre.next;
+ for(let i = 0; i < right - left; i++){
+ let next = cur.next;
+ cur.next = pre;
+ pre = cur;
+ cur = next;
+ }
+ return dummy.next;
+}
+
+var mergeKList1 = (lists) => {
+ return sortList(lists, 0, lists.length - 1);
+}
+
+function sortList(lists, left, right){
+ if(left === right) return lists[left];
+ let mid = Math.floor((left + right) / 2);
+ let l = sortList(lists, 0, mid);
+ let r = sortList(lists, mid + 1, right);
+ return mergeTwoList2(l, r);
+}
+
+function mergeTwoList2(l1, l2){
+ let dummy = new ListNode(-1);
+ let pre = dummy;
+ while(l1 && l2){
+ if(l1.val <= l2.val){
+ pre.next = l1;
+ l1 = l1.next;
+ }else{
+ pre.next = l2;
+ l2 = l2.next;
+ }
+ pre = pre.next;
+ }
+ return dummy.next;
+}
+
+var sortList = (head) => {
+ return mergeSort(head);
+}
+
+const mergeSort = head => {
+ if(head === null || head.next === null){
+ return head;
+ }
+ let slow = head;
+ let fast = head.next.next;
+ while(fast !== null && fast.next !== null){
+ slow = slow.next;
+ fast = fast.next.next;
+ }
+ let mid = slow.next;
+ slow.next = null;
+ let left = mergeSort(head);
+ let right = mergeSort(mid);
+ return mergeKList(left, right)
+}
+
+
+const circle = head => {
+ if(!head) return false;
+ let slow = head;
+ let fast = head;
+ while(fast && fast.next){
+ slow = slow.next;
+ fast = fast.next.next;
+ if(slow === fast) return true;
+ }
+ return false;
+}
+
+const sortList = head => {
+ return mergeSort(head);
+}
+
+function mergeSort(head){
+ if(head == null || head.next == null){
+ return head;
+ }
+ let slow = head;
+ let fast = head.next.next;
+ while(fast && fast.next){
+ slow = slow.next;
+ fast = fast.next.next;
+ }
+ let mid = slow.next;
+ slow.next = null;
+ let l1 = mergeSort(head);
+ let l2 = mergeSort(mid);
+ return mergeTwoList(l1, l2);
+}
+
+const sortList = (head) => {
+ return mergeSort(head);
+}
+
+function mergeSort(head){
+ if(head === null || head.next === null){
+ return head;
+ }
+ let slow = head;
+ let fast = head.next.next;
+ while(fast && fast.next){
+ slow = slow.next;
+ fast = fast.next.next;
+ }
+ let mid = slow.next;
+ slow.next = null;
+ let l1 = mergeSort(head);
+ let l2 = mergeSort(mid);
+ return mergeTwoList(l1, l2);
+}
+
+const hasCircle = head => {
+ let slow = head;
+ let fast = head.next;
+ while(slow !== fast){
+ slow = slow.next;
+ fast = fast.next.next;
+ if(slow === fast){
+ slow = head;
+ while(slow !== fast){
+ slow = slow.next;
+ fast = fast.next;
+ }
+ return slow;
+ }
+ }
+ return null;
+}
+
+const removeDN = function(head, n){
+ let dummy = new ListNode(-1);
+ dummy.next = head;
+ let fast = dummy;
+ let slow = dummy;
+
+ for(let i = 0; i < n; i++){
+ fast = fast.next;
+ }
+
+ while(fast.next !== null){
+ slow = slow.next;
+ fast = fast.next;
+ }
+ slow.next = slow.next.next;
+ return dummy.next;
+}
+
+const hasCircle2 = head => {
+ let slow = head;
+ let fast = head;
+ while(fast && fast.next){
+ slow = slow.next;
+ fast = fast.next;
+ if(slow === fast){
+ slow = head;
+ while(slow !== fast){
+ slow = slow.next;
+ fast = fast.next;
+ }
+ return slow;
+ }
+ }
+ return null;
+}
+
+const intersaction = (headA, headB) => {
+ if(headA === null || headB === null) return null;
+ let pA = headA;
+ let pB = headB;
+
+ while(pA !== pB){
+ pA = pA === null ? headB : pA.next;
+ pB = pB === null ? headA : pB.next;
+ }
+
+ return pA;
+
+}
+
+const deleteN = (head, n) => {
+ let dummy = new ListNode(-1);
+ dummy.next = head;``
+ let slow = dummy;
+ let fast = dummy;
+
+ for(let i = 0; i < n; i++){
+ fast = fast.next;
+ }
+
+ while(fast.next !== null){
+ slow = slow.next;
+ fast = fast.next;
+ }
+ slow.next = slow.next.next;
+ return dummy.next;
+}
+
+const intersaction1 = (headA, headB) => {
+ if(headA === null || headB === null) return null;
+ let pA = headA;
+ let pB = headB;
+
+ while(pA !== pB){
+ pA = pA === null ? headB : headA;
+ pB = pB === null ? headA : headB;
+ }
+
+ return pA;
+}
+
+const resort = (head) => {
+ let slow = head;
+ let fast = head;
+ while(fast && fast.next){
+ slow = slow.next;
+ fast = fast.next.next;
+ }
+ let cur = slow.next;
+ slow.next = null;
+ let pre = null;
+ while(cur !== null){
+ let next = cur.next;
+ cur.next = pre;
+ pre = cur;
+ cur = next;
+ }
+
+ let head1 = head;
+ let head2 = pre;
+ while(head1 && head2){
+ let next1 = head1.next;
+ let next2 = head2.next;
+ head1.next = head2;
+ head1 = next1;
+ head2.next = next1;
+ head2 = next2;
+ }
+}
+
+const resortList = head => {
+ let slow = head;
+ let fast = head;
+ while(fast && fast.next){
+ slow = slow.next;
+ fast = fast.next.next;
+ }
+ let pre = null;
+ let cur = slow.next;
+ slow.next = null;
+
+ while(cur !== null){
+ let next = cur.next;
+ cur.next = pre;
+ pre = cur;
+ cur = next;
+ }
+
+ let l1 = head;
+ let l2 = pre;
+ while(l1 && l2){
+ let next1 = l1.next;
+ let next2 = l2.next;
+ l1.next = l2;
+ l1 = next1;
+ l2.next = next1;
+ l2 = next2;
+ }
+}
+
+const LRUCache = function(capacity){
+ this.capacity = capacity;
+ this.map = new Map();
+ this.head = new ListNode(-1, -1);
+ this.tail = new ListNode(-1, -1);
+ this.head.next = this.tail;
+ this.tail.pre = this.head;
+}
+
+function ListNode(key, val){
+ this.key = key;
+ this.val = val;
+ this.pre = null;
+ this.next = null;
+}
+
+LRUCache.prototype.get = function(key){
+ if(!this.map.has(key)) return -1;
+ const node = this.map.get(key);
+ this.moveToHead(node);
+ return node;
+}
+
+LRUCache.prototype.put = function(key, value){
+ if(this.map.has(key)){
+ const node = this.map.get(key);
+ node.val = value;
+ this.moveToHead(node);
+ }else{
+ if(this.capacity === this.map.size){
+ const lastNode = this.tail.prev;
+ this.removeNode(lastNode);
+ this.map.delete(lastNode.key);
+ }
+ const node = new ListNode(key, value);
+ this.addNodeToHead(node);
+ this.map.set(key, value);
+ }
+}
+
+LRUCache.prototype.moveToHead = function(node){
+ this.removeNode(node);
+ this.addNodeToHead(node);
+}
+
+LRUCache.prototype.removeNode = function(node){
+ node.pre.next = node.next;
+ node.next.pre = node.pre;
+}
+
+LRUCache.prototype.addNodeTohead = function(node){
+ node.next = this.head.next;
+ node.pre = this.head;
+ this.head.next = node;
+ this.next.pre = node;
+}
+
+const deleteDuplicates = (head) => {
+ let dummy = new ListNode(-1);
+ dummy.next = head;
+ let pre = dummy;
+ while(pre.next !== null && pre.next.next !== null){
+ if(pre.next.val === pre.next.next.val){
+ let val = pre.next.val;
+ while(pre.next !== null && val === pre.next.val){
+ pre.next = pre.next.next;
+ }
+ }else{
+ pre = pre.next;
+ }
+ }
+ return dummy.next;
+}
\ No newline at end of file
diff --git "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\257\207/\344\272\214\345\217\211\346\240\221.md" "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\257\207/\344\272\214\345\217\211\346\240\221.md"
index 961f54b..bc51291 100644
--- "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\257\207/\344\272\214\345\217\211\346\240\221.md"
+++ "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\257\207/\344\272\214\345\217\211\346\240\221.md"
@@ -9,6 +9,195 @@
- 以根访问顺序决定是什么遍历
- 左子树都是优先右子树
+## 二叉树通解与模板
+
+二叉树题目的本质只有两点:
+1. **怎么遍历?** (DFS 还是 BFS?前/中/后序?)
+2. **在遍历的每个节点上做什么?** (处理逻辑)
+
+### 一、 递归通解:两大思维模式
+
+递归是二叉树的灵魂。做题时不要试图跳进递归的每一层去人脑压栈,而是要利用**数学归纳法**的思维:**只关注当前节点要做什么,假定子树已经做好了**。
+
+#### 1. 模式一:遍历思维 (Traverse / Top-down)
+**核心思想**:带着一个“全局变量”或者“状态参数”跑遍整棵树,走到哪更新到哪。一般没有返回值(`void`)。
+**适用场景**:路径总和、输出所有路径、单纯的遍历打印。
+
+```js
+// 遍历思维模板
+let result; // 1. 定义全局结果
+
+var traverse = function(root) {
+ // 2. 初始化结果
+ result = [];
+ dfs(root);
+ return result;
+};
+
+var dfs = function(root) {
+ // 3. Base Case:空节点直接返回
+ if (root === null) {
+ return;
+ }
+
+ // --- 前序位置 (进入节点前) ---
+ // 这里的逻辑是:处理当前节点
+ // result.push(root.val);
+
+ dfs(root.left); // 递归遍历左子树
+ dfs(root.right); // 递归遍历右子树
+
+ // --- 后序位置 (离开节点后) ---
+ // 这里的逻辑是:如果需要在离开节点时撤销选择(回溯法)写在这里
+};
+```
+
+#### 2. 模式二:分治思维 (Divide & Conquer / Bottom-up)
+**核心思想**:**大部分二叉树难题的解法**。让左右子树分别去干活,把结果**返回**给你,你在当前节点把结果组合起来。
+**适用场景**:最大深度、是否平衡、最近公共祖先、最大路径和。
+
+**口诀**:
+1. **也就是问左右子树要什么?** (定义递归函数的返回值)
+2. **当前节点拿到左右子树的返回值,怎么处理?** (合并逻辑)
+3. **当前节点应该返回给父节点什么?** (向上层汇报)
+
+```js
+// 分治思维模板
+var divideAndConquer = function(root) {
+ // 1. Base Case
+ if (root === null) {
+ return null; // 或者 0, [], false, 根据题目要求
+ }
+
+ // 2. 分 (Divide):通过递归获得左右子树的结果
+ let leftResult = divideAndConquer(root.left);
+ let rightResult = divideAndConquer(root.right);
+
+ // 3. 治 (Conquer):在当前节点处理(合并)结果
+ // 比如:最大深度 = max(left, right) + 1
+ let currentResult = /* 根据 leftResult 和 rightResult 算出当前节点结果 */;
+
+ // 4. 返回给上一层
+ return currentResult;
+};
+```
+
+### 二、 迭代通解:层序遍历 (BFS)
+
+**核心思想**:只要题目提到“按层”、“最小步数”、“最短路径”或需要一层层处理,立刻想到 **Queue (队列)**。
+
+**模板** (背下来,直接套用):
+
+```js
+// BFS 层序遍历模板
+var levelOrder = function(root) {
+ if (root === null) return [];
+
+ const queue = [root]; // 1. 初始化队列,放入根节点
+ const res = [];
+
+ while (queue.length > 0) {
+ const size = queue.length; // 2. 锁定当前层的节点个数(必须先存size,因为queue长度会变)
+ const currentLevel = []; // 存放当前层结果
+
+ // 3. 遍历当前层的每一个节点
+ for (let i = 0; i < size; i++) {
+ const node = queue.shift(); //以此取出队头
+ currentLevel.push(node.val);
+
+ // 4. 将下一层的节点放入队列
+ if (node.left) queue.push(node.left);
+ if (node.right) queue.push(node.right);
+ }
+
+ res.push(currentLevel); // 收集每一层的结果
+ }
+ return res;
+};
+```
+
+### 三、 二叉搜索树 (BST) 特效药
+
+如果是 **BST (Binary Search Tree)** 题目,切记一个核心性质:
+> **BST 的中序遍历 是一个 有序数组**
+
+**通用解法**:
+1. **验证 BST**:中序遍历看是否递增。
+2. **BST 第 K 小元素**:中序遍历第 K 个。
+3. **BST 转累加树**:反向中序遍历 (右->根->左)。
+
+```js
+// BST 通用中序处理
+var bstProcessor = function(root) {
+ if (root === null) return;
+
+ bstProcessor(root.left);
+
+ // --- 中序位置 ---
+ // 此时已经是有序的了
+ // print(root.val)
+
+ bstProcessor(root.right);
+}
+```
+
+### 四、非递归 DFS 统一模板 (颜色标记法)
+
+**痛点**:传统非递归写法前中后序逻辑不一致(前序简单,中序需嵌套循环,后序需反转或者双栈),导致很难记忆。
+**解决方案**:**“颜色标记法”**。通过压栈 `null` (或特定标记) 来区分“访问节点”与“处理节点”。
+
+**模板口诀**:**栈是后进先出,入栈顺序与遍历顺序相反**。
+1. 遇到节点 -> 将其与子节点按 **反向顺序** 入栈。
+2. 遇到待处理节点(中节点) -> 放入栈中,并紧跟一个 `null` 标记。
+3. 遇到 `null` -> 弹出下一个节点,加入结果集。
+
+```js
+// 统一模板:只需要调整入栈顺序即可实现前中后序
+var traversal = function(root) {
+ const res = [];
+ const stack = [];
+ if (root) stack.push(root);
+
+ while (stack.length > 0) {
+ const node = stack.pop();
+
+ if (!node) { // 1. 遇到标记
+ res.push(stack.pop().val); // 2. 处理标记后的节点
+ continue;
+ }
+
+ // 3. 按照遍历顺序的 **逆序** 入栈
+ // --- 示例:中序遍历 (左 -> 中 -> 右) ---
+ // 入栈顺序:右 -> 中(标记) -> 左
+
+ if (node.right) stack.push(node.right); // 右
+
+ stack.push(node);
+ stack.push(null); // 中 (打标记,代表待处理)
+
+ if (node.left) stack.push(node.left); // 左
+ }
+ return res;
+};
+```
+
+**不同遍历顺序的入栈变换**:
+
+- **前序 (中左右)** -> 入栈:右 -> 左 -> 中(mark)
+- **中序 (左中右)** -> 入栈:右 -> 中(mark) -> 左
+- **后序 (左右中)** -> 入栈:中(mark) -> 右 -> 左
+
+### 五、 记忆心法总结
+
+1. **看到“最大”、“所有路径”** -> 优先想 **DFS** (递归)。
+ - 如果需要由下向上反推 (如树高、直径) -> **分治法 (后序)**。
+ - 如果需要自顶向下携带参数 (如路径和匹配) -> **遍历法 (前序)**。
+2. **看到“层级”、“最短”、“宽度”** -> 优先想 **BFS** (Queue)。
+3. **操作当前节点**:
+ - **前序**:哪怕我不知道子树情况,我也能先处理自己 (比如打印)。
+ - **中序**:BST 专用 (有序)。
+ - **后序**:必须等子树汇报完情况,我才能做决定 (比如删节点、算高度、LCA)。
+
#### 树结构
```tsx
@@ -58,70 +247,87 @@ let root = binaryTree(arr);
#### 前序递归
```js
-function preOrder(root) {
- if(root === null){
- return null;
+// 1. 前序递归 (PreOrder) - Template Style
+// 采用 "遍历思维" 模板
+var preorderTraversal = function(root) {
+ const res = [];
+ const dfs = (node) => {
+ if(node === null) return;
+
+ // 前序位置:处理节点
+ res.push(node.val);
+
+ dfs(node.left);
+ dfs(node.right);
}
- console.log(root.val);
- preOrder(root.left);
- preOrder(root.right);
-}
+ dfs(root);
+ return res;
+};
```
#### 前序非递归
```js
+// 2. 前序非递归 (PreOrder Iterative)
+// 方法一:标准栈写法 (Standard)
const preOrderTraversal = function (root) {
- if(root === null){
- return [];
- }
+ if(root === null) return [];
+
const res = [];
- const stack = [];
- stack.push(root);
- while(stack.length !== 0){
+ const stack = [root];
+
+ while(stack.length > 0){
const node = stack.pop();
res.push(node.val);
- if (node.right !== null) {
- stack.push(node.right);
- }
- if (node.left !== null) {
- stack.push(node.left);
- }
+
+ // 先压右,后压左 -> 出栈就是 左,右
+ if (node.right !== null) stack.push(node.right);
+ if (node.left !== null) stack.push(node.left);
}
return res;
}
+// 方法二:颜色标记法 (Unified) - 见下文通用模板章节
```
#### 中序递归
```js
-function inOrder(root){
- if(root === null){
- return null;
+// 3. 中序递归 (InOrder) - Template Style
+var inorderTraversal = function(root){
+ const res = [];
+ const dfs = (node) => {
+ if(node === null) return;
+
+ dfs(node.left);
+ // 中序位置
+ res.push(node.val);
+ dfs(node.right);
}
- inOrder(root.left);
- console.log(root.val);
- inOrder(root.right);
+ dfs(root);
+ return res;
}
```
#### 中序非递归
```js
+// 4. 中序非递归 (InOrder Iterative)
+// 标准栈写法
const inOrderTraversal = function(root){
- if (root === null) {
- return [];
- }
const res = [];
const stack = [];
let node = root;
- while(stack.length!==0 || node!==null){
+
+ while(stack.length > 0 || node !== null){
+ // 不断往左走
while(node !== null){
stack.push(node);
node = node.left;
}
+ // 走到头了,弹出并处理
node = stack.pop();
res.push(node.val);
+ // 转向右边
node = node.right;
}
return res;
@@ -131,36 +337,43 @@ const inOrderTraversal = function(root){
#### 后序递归
```js
-function postOrder(root){
- if(root === null){
- return null;
+// 5. 后序递归 (PostOrder) - Template Style
+var postorderTraversal = function(root){
+ const res = [];
+ const dfs = (node) => {
+ if(node === null) return;
+
+ dfs(node.left);
+ dfs(node.right);
+ // 后序位置
+ res.push(node.val);
}
- postOrder(root.left);
- postOrder(root.right);
- console.log(root.val);
+ dfs(root);
+ return res;
}
```
#### 后序非递归
```js
-const postOrderTraversal = function(root){ //翻转非递归 后序遍历
- if (root === null) {
- return [];
- }
+// 6. 后序非递归 (PostOrder Iterative)
+// 方法:前序(中左右) -> 变形(中右左) -> 反转res(左右中)
+const postOrderTraversal = function(root){
+ if (root === null) return [];
+
const res = [];
- const stack = [];
- stack.push(root);
- while(stack.length !== 0){
- let node = stack.pop();
+ const stack = [root];
+
+ while(stack.length > 0){
+ const node = stack.pop();
res.push(node.val);
- if (node.left !== null) {
- stack.push(node.left);
- }
- if (node.right !== null) {
- stack.push(node.right);
- }
+
+ // 先压左,后压右 -> 出栈是 右,左
+ // 配合 push(val) -> 结果是 中,右,左
+ if (node.left !== null) stack.push(node.left);
+ if (node.right !== null) stack.push(node.right);
}
+ // 反转 -> 左,右,中
return res.reverse();
}
```
@@ -168,39 +381,35 @@ const postOrderTraversal = function(root){ //翻转非递归 后序遍历
#### 深度搜索DFS
```js
-const dfsUpToDown = function(root){ //递归,从上到下
+// 7. 深度搜索 DFS (Template)
+// 等同于 "遍历思维" 模板
+const dfsRecursive = function(root){
const res = [];
- dfs(root, res);
- return res;
-}
-
-const dfs = function(node, res){
- if (node === null) {
- return null;
+ const dfs = (node) => {
+ if (node === null) return;
+
+ res.push(node.val); // 前序位置
+ dfs(node.left);
+ dfs(node.right);
}
- res.push(node.val);
- dfs(node.left, res);
- dfs(node.right, res);
-}
-
-const dfsDownToUp = function(root){ //从下到上
- return divideAndConquer(root);
+ dfs(root);
+ return res;
}
-const divideAndConquer = function(node){ //分治法
+// 分治法 (Divide & Conquer)
+// 适用于:自底向上,或者合并左右子树结果
+const divideAndConquer = function(node){
const res = [];
- if (node === null) {
- return null;
- }
- let left = divideAndConquer(node.left);
- let right = divideAndConquer(node.right);
+ if (node === null) return []; // 注意:分治法的 Base Case 返回值通常取决于题目,这里也是[]
+
+ const left = divideAndConquer(node.left);
+ const right = divideAndConquer(node.right);
+
+ // 合并逻辑 (Conquer)
res.push(node.val);
- if (left !== null) {
- res = res.concat(left.flat());
- }
- if (right !== null) {
- res = res.concat(right.flat());
- }
+ if(left) res.push(...left);
+ if(right) res.push(...right);
+
return res;
}
```
@@ -208,22 +417,31 @@ const divideAndConquer = function(node){ //分治法
#### 广度搜索BFS
```js
-const bfs = function(root){
- let res = [];
- const queue = [];
- queue.push(root);
- while(queue.length !== 0){
- const node = queue.shift();
- res.push(node.val);
- if (node.left !== null) {
- queue.push(node.left);
- }
- if (node.right !== null) {
- queue.push(node.right);
+// 8. 广度搜索 BFS (Template)
+// 使用标准的队列模板
+var levelOrder = function(root) {
+ if (root === null) return [];
+
+ const queue = [root];
+ const res = [];
+
+ while (queue.length > 0) {
+ // level size
+ const size = queue.length;
+ // current level nodes
+ const currentLevel = [];
+
+ for (let i = 0; i < size; i++) {
+ const node = queue.shift();
+ currentLevel.push(node.val);
+
+ if (node.left) queue.push(node.left);
+ if (node.right) queue.push(node.right);
}
+ res.push(currentLevel); // Store level by level
}
return res;
-}
+};
```
[104.二叉树的最大深度](https://leetcode-cn.com/problems/maximum-depth-of-binary-tree/)
@@ -241,11 +459,17 @@ const bfs = function(root){
> 返回它的最大深度 3 。
```js
-const maxDepth = function(root) { //递归
+const maxDepth = function(root) {
+ // 1. Base Case
if(root === null){
return 0;
}
- return Math.max(maxDepth(root.left), maxDepth(root.right))+1;
+ // 2. Divide
+ const leftDepth = maxDepth(root.left);
+ const rightDepth = maxDepth(root.right);
+
+ // 3. Conquer (Merge)
+ return Math.max(leftDepth, rightDepth) + 1;
};
```
@@ -255,27 +479,26 @@ const maxDepth = function(root) { //递归
```js
const isBalanced = function(root) {
- if(maxDepth(root) === -1) {
- return false
- }
- return true
+ // 利用后续分治,如果返回 -1 代表不平衡
+ return maxDepth(root) !== -1;
};
const maxDepth = function(root) {
+ // 1. Base Case
if(root === null) {
- return 0
- }
- const left = maxDepth(root.left)
- const right = maxDepth(root.right)
- if(left === -1 || right === -1 || Math.abs(right - left) > 1) { //判断左右子树的高度
- return -1
- }
- if(left > right) {
- return left + 1
- }else{
- return right + 1
+ return 0;
}
-
+ // 2. Divide
+ const left = maxDepth(root.left);
+ const right = maxDepth(root.right);
+
+ // 3. Conquer (Check Balance)
+ // 如果子树已经不平衡,或者当前节点不平衡,直接返回-1标记
+ if(left === -1 || right === -1 || Math.abs(left - right) > 1) {
+ return -1;
+ }
+
+ return Math.max(left, right) + 1;
}
```
@@ -287,18 +510,29 @@ const maxDepth = function(root) {
```js
var maxPathSum = function (root) {
- let maxSum = Number.MIN_SAFE_INTEGER
- function maxGain(node) {
- if (!node) {
- return 0
+ let maxSum = Number.MIN_SAFE_INTEGER;
+
+ // 分治函数:计算以当前节点为根的单边最大路径和
+ const dfs = (node) => {
+ // 1. Base Case
+ if (node === null) {
+ return 0;
}
- const left = maxGain(node.left)
- const right = maxGain(node.right)
- maxSum = Math.max(maxSum, node.val, node.val + left + right, node.val + left, node.val + right)
- return Math.max(node.val, node.val + left, node.val + right)
- }
- maxGain(root)
- return maxSum
+
+ // 2. Divide: 计算左右子树的单边最大贡献(负数也不选)
+ const leftGain = Math.max(dfs(node.left), 0);
+ const rightGain = Math.max(dfs(node.right), 0);
+
+ // 3. Conquer (Update Global Max): 更新全局最大路径和(包含当前节点和左右子树)
+ const currentPathSum = node.val + leftGain + rightGain;
+ maxSum = Math.max(maxSum, currentPathSum);
+
+ // 4. Return: 返回当前节点的最大单边路径和给父节点
+ return node.val + Math.max(leftGain, rightGain);
+ }
+
+ dfs(root);
+ return maxSum;
};
```
@@ -308,20 +542,23 @@ var maxPathSum = function (root) {
```js
var lowestCommonAncestor = function (root, p, q) {
- let ans = root
- const dfs = (node) => {
- if (node === null) {
- return false
- }
- const lson = dfs(node.left)
- const rson = dfs(node.right)
- if (((node.val === p.val || node.val === q.val) && (lson || rson)) || (lson && rson)) {
- ans = node
- }
- return (node.val === p.val || node.val === q.val) || lson || rson
+ // 1. Base Case
+ // 如果是空,或者找到了 p 或 q,直接返回当前节点
+ if (root === null || root === p || root === q) {
+ return root;
}
- dfs(root)
- return ans
+
+ // 2. Divide
+ const left = lowestCommonAncestor(root.left, p, q);
+ const right = lowestCommonAncestor(root.right, p, q);
+
+ // 3. Conquer
+ // 如果左右都找到了,说明当前节点是 LCA
+ if (left !== null && right !== null) {
+ return root;
+ }
+ // 否则返回非空的那个(即找到了 p 或 q 的那一边)
+ return left !== null ? left : right;
};
```
@@ -330,29 +567,149 @@ var lowestCommonAncestor = function (root, p, q) {
给你一个二叉树,请你返回其按 **层序遍历** 得到的节点值。
```js
+// 严格套用 BFS 模板
var levelOrder = function (root) {
- if (root === null) {
- return []
- }
- const res = []
- const queue = []
- queue.push(root)
+ if (root === null) return [];
+
+ const queue = [root];
+ const res = [];
+
while (queue.length > 0) {
- const currentQueueSize = queue.length
- const list = [] // 存放每一层的节点值
- for (let i = 1; i <= currentQueueSize; i++) {
- const node = queue.shift()
- list.push(node.val)
- if (node.left) {
- queue.push(node.left)
- }
- if (node.right) {
- queue.push(node.right)
- }
+ // 一次处理一层
+ const size = queue.length;
+ const level = [];
+
+ for (let i = 0; i < size; i++) {
+ const node = queue.shift();
+ level.push(node.val);
+
+ if (node.left) queue.push(node.left);
+ if (node.right) queue.push(node.right);
}
- res.push(list)
+ res.push(level);
}
- return res
+ return res;
+};
+```
+
+[101. 对称二叉树](https://leetcode-cn.com/problems/symmetric-tree/)
+
+给定一个二叉树,检查它是否是镜像对称的。
+
+```js
+var isSymmetric = function(root) {
+ if(root === null) return true;
+
+ // 递归判断两个子树是否互为镜像
+ const check = (left, right) => {
+ // 1. Base Case
+ if (left === null && right === null) return true; // 都为空,对称
+ if (left === null || right === null) return false; // 一个空一个不空,不对称
+
+ // 2. Divide: 判断
+ // A. 根节点值是否相同
+ if (left.val !== right.val) return false;
+
+ // B. 递归比较:左子树的左 vs 右子树的右,左子树的右 vs 右子树的左
+ return check(left.left, right.right) && check(left.right, right.left);
+ }
+
+ return check(root.left, root.right);
+};
+```
+
+[LCR 143. 树的子结构 (剑指 Offer 26)](https://leetcode.cn/problems/shu-de-zi-jie-gou-lcof/)
+
+输入两棵二叉树 A 和 B,判断 B 是不是 A 的子结构。(约定空树不是任意一个树的子结构)
+**注意**:子结构不等于子树。子结构只需要“局部匹配”即可,不需要匹配到叶子节点。
+
+```js
+var isSubStructure = function(A, B) {
+ // 特殊约定:空树不是子结构
+ if (!A || !B) return false;
+
+ // 1. Base Case: 以当前节点匹配
+ // 2. Divide: 否则去左子树找,或者去右子树找
+ return isSame(A, B) || isSubStructure(A.left, B) || isSubStructure(A.right, B);
+};
+
+const isSame = (A, B) => {
+ // 关键点:B 匹配完了,说明找到了,返回 true
+ if (!B) return true;
+ // B 没完但 A 完了,说明匹配不上,返回 false
+ if (!A) return false;
+
+ // 值不同,匹配失败
+ if (A.val !== B.val) return false;
+
+ // 必须左右同时匹配
+ return isSame(A.left, B.left) && isSame(A.right, B.right);
+}
+```
+
+[129. 求根节点到叶节点数字之和](https://leetcode-cn.com/problems/sum-root-to-leaf-numbers/)
+
+给定一个二叉树,它的每个结点都存放一个 0-9 的数字,每条从根到叶子节点的路径都代表一个数字。计算从根到叶子节点生成的所有数字之和。
+
+```js
+var sumNumbers = function(root) {
+ // 套用【遍历思维】模板 (Traverse)
+ // 这里的“全局变量”是 dfs 函数的参数,也可以写在外面
+ let sum = 0;
+
+ const dfs = (node, curNum) => {
+ // 1. Base Case
+ if (node === null) return;
+
+ // 2. 前序位置:更新当前路径的数字
+ curNum = curNum * 10 + node.val;
+
+ // 3. 判断叶子节点:如果到了叶子,累加结果
+ if (node.left === null && node.right === null) {
+ sum += curNum;
+ return;
+ }
+
+ // 4. 继续遍历左右子树
+ dfs(node.left, curNum);
+ dfs(node.right, curNum);
+ }
+
+ dfs(root, 0);
+ return sum;
+};
+```
+
+[105. 从前序与中序遍历序列构造二叉树](https://leetcode-cn.com/problems/construct-binary-tree-from-preorder-and-inorder-traversal/)
+
+给定一棵树的前序遍历 `preorder` 与中序遍历 `inorder`。请构造二叉树并返回其根节点。
+
+```js
+var buildTree = function(preorder, inorder) {
+ // 套用【分治思维】模板
+ // 1. Base Case: 序列为空,返回 null
+ if (preorder.length === 0 || inorder.length === 0) {
+ return null;
+ }
+
+ // 2. 根节点:前序遍历的第一个元素
+ const rootVal = preorder[0];
+ const root = new TreeNode(rootVal);
+
+ // 3. 找到根节点在中序遍历中的位置,以此划分左右子树
+ const index = inorder.indexOf(rootVal);
+
+ // 4. Divide: 切割数组,递归构建左右子树
+ // 左子树的中序:[0, index)
+ // 左子树的前序:[1, index + 1) (长度要和中序一致)
+ root.left = buildTree(preorder.slice(1, index + 1), inorder.slice(0, index));
+
+ // 右子树的中序:[index + 1, end)
+ // 右子树的前序:[index + 1, end)
+ root.right = buildTree(preorder.slice(index + 1), inorder.slice(index + 1));
+
+ // 5. Return: 返回构建好的根节点
+ return root;
};
```
@@ -361,28 +718,59 @@ var levelOrder = function (root) {
给定一个二叉树,返回其节点值自底向上的层次遍历。(即按从叶子节点所在层到根节点所在的层,逐层从左向右遍历)
```js
-if (root === null) {
- return []
+var levelOrderBottom = function(root) {
+ // 套用 BFS 模板,只是最后存结果用 unshift
+ if (root === null) return [];
+
+ const queue = [root];
+ const res = [];
+
+ while (queue.length > 0) {
+ const size = queue.length;
+ const level = [];
+
+ for (let i = 0; i < size; i++) {
+ const node = queue.shift();
+ level.push(node.val);
+ if (node.left) queue.push(node.left);
+ if (node.right) queue.push(node.right);
+ }
+ res.unshift(level); // 与标准 BFS 唯一的区别:从头部插入结果
}
- const res = []
- const queue = []
- queue.push(root)
+ return res;
+};
+```
+
+[199. 二叉树的右视图](https://leetcode-cn.com/problems/binary-tree-right-side-view/)
+
+给定一个二叉树的 根节点 root,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。
+
+```js
+var rightSideView = function(root) {
+ // 套用 BFS 模板
+ if (root === null) return [];
+
+ const queue = [root];
+ const res = [];
+
while (queue.length > 0) {
- const currentQueueSize = queue.length
- const list = []
- for (let i = 1; i <= currentQueueSize; i++) {
- const node = queue.shift()
- list.push(node.val)
- if (node.left) {
- queue.push(node.left)
- }
- if (node.right) {
- queue.push(node.right)
+ const size = queue.length;
+ // 这一层的最后一个元素,就是右视图看到的元素
+
+ for (let i = 0; i < size; i++) {
+ const node = queue.shift();
+
+ // 如果是当前层的最后一个节点,放入结果集
+ if (i === size - 1) {
+ res.push(node.val);
}
+
+ if (node.left) queue.push(node.left);
+ if (node.right) queue.push(node.right);
}
- res.unshift(list) //unshift 从头部插入
}
- return res
+ return res;
+};
```
[103. 二叉树的锯齿形层序遍历](https://leetcode-cn.com/problems/binary-tree-zigzag-level-order-traversal/)
@@ -391,33 +779,78 @@ if (root === null) {
```js
var zigzagLevelOrder = function(root) {
- if (root === null) {
- return []
- }
- const res = []
- const queue = []
- queue.push(root)
+ // 套用 BFS 模板,处理每一层的顺序不同
+ if (root === null) return [];
+
+ const queue = [root];
+ const res = [];
+ let isOrderLeft = true; // 标记方向
+
while (queue.length > 0) {
- const currentQueueSize = queue.length
- const list = []
- for (let i = 1; i <= currentQueueSize; i++) {
- const node = queue.shift()
- list.push(node.val)
- if (node.left) {
- queue.push(node.left)
- }
- if (node.right) {
- queue.push(node.right)
+ const size = queue.length;
+ const level = []; // 使用双端队列思想 (这里简单用数组配合反转)
+
+ for (let i = 0; i < size; i++) {
+ const node = queue.shift();
+ if (isOrderLeft) {
+ level.push(node.val);
+ } else {
+ level.unshift(node.val); // 倒序就从头插,或者等push完再reverse
}
+
+ if (node.left) queue.push(node.left);
+ if (node.right) queue.push(node.right);
}
- res.push(list)
+ res.push(level);
+ isOrderLeft = !isOrderLeft;
}
- return res.map((item, index) => {
- if(index % 2 === 1) {
- item = item.reverse()
+ return res;
+};
+```
+
+[662. 二叉树最大宽度](https://leetcode-cn.com/problems/maximum-width-of-binary-tree/)
+
+给定一个二叉树,编写一个函数来获取这个树的最大宽度。树的宽度是所有层中的最大宽度。
+**注意**:本题的关键在于节点可能是 `null`,但计算宽度时要视为存在(空节点也要占位)。
+
+```js
+var widthOfBinaryTree = function(root) {
+ // 套用 BFS 模板,但需要给节点编号
+ // 编号规则:root 为 1,左孩子为 2*i,右孩子为 2*i+1
+ // 关键点:JS数字如果是大数,会精度丢失,需要用 BigInt
+
+ if (root === null) return 0;
+
+ // queue 存放 [node, index]
+ // 初始 index 用 1n (BigInt)
+ const queue = [[root, 1n]];
+ let maxWidth = 0;
+
+ while (queue.length > 0) {
+ const size = queue.length;
+
+ // 记录当前层的 最左索引 和 最右索引
+ // 肯定分别是队头和队尾(因为是层序的)
+ // 但注意:for循环执行完后,queue 里剩下的就是下一层的了,所以要在本层开始前记录,或者在循环中记录
+
+ let leftIndex, rightIndex;
+ // 如果想要方便,可以在循环开始前取头尾
+ // 队头就是本层最左,队尾是本层最右
+ if (size > 0) {
+ leftIndex = queue[0][1];
+ rightIndex = queue[queue.length - 1][1];
+ // 计算宽度,并转换为 Number
+ maxWidth = Math.max(maxWidth, Number(rightIndex - leftIndex + 1n));
+ }
+
+ for (let i = 0; i < size; i++) {
+ const [node, index] = queue.shift();
+
+ if (node.left) queue.push([node.left, index * 2n]);
+ if (node.right) queue.push([node.right, index * 2n + 1n]);
}
- return item
- })
+ }
+ return maxWidth;
};
```
@@ -431,41 +864,27 @@ var zigzagLevelOrder = function(root) {
所有左子树和右子树自身必须也是二叉搜索树。
```js
-var isValidBST = function (root) { //中序遍历,值会从小到大排列
- const list = []
- inOrder(root, list)
- for(let i=0;i= list[i+1]){
- return false
- }
- }
- return true
-};
-
-const inOrder = (node, list) => {
- if(node === null) {
- return null
- }
- const left = inOrder(node.left,list)
- list.push(node.val)
- const right = inOrder(node.right,list)
-}
-```
-
-```js
-var isValidBST = function (root) { //递归写法
- return helper(root, -Infinity, Infinity)
+var isValidBST = function (root) {
+ // 利用 BST 性质:中序遍历是有序的
+ let pre = -Infinity;
+
+ // 返回值:是否合法
+ const dfs = (node) => {
+ if (node === null) return true;
+
+ // 左
+ if (!dfs(node.left)) return false;
+
+ // 根 (检查是否大于前一个值)
+ if (node.val <= pre) return false;
+ pre = node.val;
+
+ // 右
+ return dfs(node.right);
+ }
+
+ return dfs(root);
};
-
-const helper = (node, smaller, bigger) => {
- if (node === null) {
- return true
- }
- if (node.val <= smaller || node.val >= bigger) {
- return false
- }
- return helper(node.left, smaller, node.val) && helper(node.right, node.val, bigger)
-}
```
[701. 二叉搜索树中的插入操作](https://leetcode-cn.com/problems/insert-into-a-binary-search-tree/)
@@ -474,32 +893,35 @@ const helper = (node, smaller, bigger) => {
```js
var insertIntoBST = function (root, val) {
+ // 递归查找插入位置,天然利用 BST 的性质
if (root === null) {
- return new TreeNode(val)
+ return new TreeNode(val);
}
+
+ // 这是一个只遍历单边子树的过程,类似二分查找
if (root.val > val) {
- root.left = insertIntoBST(root.left, val)
+ root.left = insertIntoBST(root.left, val);
} else {
- root.right = insertIntoBST(root.right, val)
+ root.right = insertIntoBST(root.right, val);
}
- return root
+ return root;
};
```
-## 总结
-
-- 掌握二叉树递归与非递归遍历
-- 理解 DFS 前序遍历与分治法
-- 理解 BFS 层次遍历
-
## 练习
- [maximum-depth-of-binary-tree](https://leetcode-cn.com/problems/maximum-depth-of-binary-tree/)
- [balanced-binary-tree](https://leetcode-cn.com/problems/balanced-binary-tree/)
+- [symmetric-tree](https://leetcode-cn.com/problems/symmetric-tree/)
+- [tree-substructure](https://leetcode-cn.com/problems/shu-de-zi-jie-gou-lcof/)
+- [sum-root-to-leaf-numbers](https://leetcode-cn.com/problems/sum-root-to-leaf-numbers/)
+- [construct-binary-tree-from-preorder-and-inorder-traversal](https://leetcode-cn.com/problems/construct-binary-tree-from-preorder-and-inorder-traversal/)
- [binary-tree-maximum-path-sum](https://leetcode-cn.com/problems/binary-tree-maximum-path-sum/)
- [lowest-common-ancestor-of-a-binary-tree](https://leetcode-cn.com/problems/lowest-common-ancestor-of-a-binary-tree/)
- [binary-tree-level-order-traversal](https://leetcode-cn.com/problems/binary-tree-level-order-traversal/)
- [binary-tree-level-order-traversal-ii](https://leetcode-cn.com/problems/binary-tree-level-order-traversal-ii/)
+- [binary-tree-right-side-view](https://leetcode-cn.com/problems/binary-tree-right-side-view/)
- [binary-tree-zigzag-level-order-traversal](https://leetcode-cn.com/problems/binary-tree-zigzag-level-order-traversal/)
+- [maximum-width-of-binary-tree](https://leetcode-cn.com/problems/maximum-width-of-binary-tree/)
- [validate-binary-search-tree](https://leetcode-cn.com/problems/validate-binary-search-tree/)
- [insert-into-a-binary-search-tree](https://leetcode-cn.com/problems/insert-into-a-binary-search-tree/)
\ No newline at end of file
diff --git "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\257\207/\346\240\210\345\222\214\351\230\237\345\210\227.md" "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\257\207/\346\240\210\345\222\214\351\230\237\345\210\227.md"
index 1e16a70..5e19e03 100644
--- "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\257\207/\346\240\210\345\222\214\351\230\237\345\210\227.md"
+++ "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\257\207/\346\240\210\345\222\214\351\230\237\345\210\227.md"
@@ -296,6 +296,115 @@ MyQueue.prototype.empty = function () {
};
```
+### 拓扑排序
+
+拓扑排序用于解决有向无环图(DAG)的排序问题,常见应用场景:课程安排、任务调度、依赖分析等。核心思想是通过 BFS(入度表)或 DFS(检测环)判断图中是否存在环。
+
+##### [207. 课程表](https://leetcode-cn.com/problems/course-schedule/)
+
+你这个学期必须选修 `numCourses` 门课程,记为 `0` 到 `numCourses - 1`。在选修某些课程之前需要一些先修课程。请你判断是否可能完成所有课程的学习。
+
+**方法一:BFS(Kahn算法 - 入度表)**
+
+```js
+var canFinish = function(numCourses, prerequisites) {
+ const inDegree = new Array(numCourses).fill(0) // 入度表
+ const graph = new Map() // 邻接表
+
+ // 建图
+ for (const [course, pre] of prerequisites) {
+ inDegree[course]++
+ if (!graph.has(pre)) graph.set(pre, [])
+ graph.get(pre).push(course)
+ }
+
+ // 将入度为0的节点入队
+ const queue = []
+ for (let i = 0; i < numCourses; i++) {
+ if (inDegree[i] === 0) queue.push(i)
+ }
+
+ let count = 0
+ while (queue.length) {
+ const cur = queue.shift()
+ count++
+ const neighbors = graph.get(cur) || []
+ for (const next of neighbors) {
+ inDegree[next]--
+ if (inDegree[next] === 0) queue.push(next)
+ }
+ }
+
+ return count === numCourses // 能完成所有课程说明无环
+}
+```
+
+**方法二:DFS(三色标记检测环)**
+
+```js
+var canFinish = function(numCourses, prerequisites) {
+ const graph = new Map()
+ for (const [course, pre] of prerequisites) {
+ if (!graph.has(pre)) graph.set(pre, [])
+ graph.get(pre).push(course)
+ }
+
+ // 0: 未访问, 1: 访问中, 2: 已完成
+ const visited = new Array(numCourses).fill(0)
+
+ const hasCycle = (node) => {
+ if (visited[node] === 1) return true // 发现环
+ if (visited[node] === 2) return false // 已处理
+
+ visited[node] = 1
+ for (const next of (graph.get(node) || [])) {
+ if (hasCycle(next)) return true
+ }
+ visited[node] = 2
+ return false
+ }
+
+ for (let i = 0; i < numCourses; i++) {
+ if (hasCycle(i)) return false
+ }
+ return true
+}
+```
+
+##### [210. 课程表 II](https://leetcode-cn.com/problems/course-schedule-ii/)
+
+返回你为了学完所有课程所安排的学习顺序。如果不可能完成所有课程,返回空数组。
+
+```js
+var findOrder = function(numCourses, prerequisites) {
+ const inDegree = new Array(numCourses).fill(0)
+ const graph = new Map()
+
+ for (const [course, pre] of prerequisites) {
+ inDegree[course]++
+ if (!graph.has(pre)) graph.set(pre, [])
+ graph.get(pre).push(course)
+ }
+
+ const queue = []
+ for (let i = 0; i < numCourses; i++) {
+ if (inDegree[i] === 0) queue.push(i)
+ }
+
+ const result = []
+ while (queue.length) {
+ const cur = queue.shift()
+ result.push(cur)
+ for (const next of (graph.get(cur) || [])) {
+ inDegree[next]--
+ if (inDegree[next] === 0) queue.push(next)
+ }
+ }
+
+ return result.length === numCourses ? result : []
+}
+```
+
##### [542. 01 矩阵](https://leetcode-cn.com/problems/01-matrix/)
```js
@@ -336,6 +445,9 @@ var updateMatrix = function (matrix) {
- 利用栈 DFS 深度搜索
- 熟悉队列的使用场景
- 利用队列 BFS 广度搜索
+- 掌握拓扑排序
+ - BFS(Kahn算法):入度表 + 队列
+ - DFS:三色标记检测环
## 练习
@@ -347,4 +459,6 @@ var updateMatrix = function (matrix) {
- [number-of-islands](https://leetcode-cn.com/problems/number-of-islands/)
- [largest-rectangle-in-histogram](https://leetcode-cn.com/problems/largest-rectangle-in-histogram/)
- [implement-queue-using-stacks](https://leetcode-cn.com/problems/implement-queue-using-stacks/)
-- [01-matrix](https://leetcode-cn.com/problems/01-matrix/)
\ No newline at end of file
+- [01-matrix](https://leetcode-cn.com/problems/01-matrix/)
+- [course-schedule](https://leetcode-cn.com/problems/course-schedule/)
+- [course-schedule-ii](https://leetcode-cn.com/problems/course-schedule-ii/)
\ No newline at end of file
diff --git "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\257\207/\351\223\276\350\241\250.md" "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\257\207/\351\223\276\350\241\250.md"
index 16cfddb..fcaa46f 100644
--- "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\257\207/\351\223\276\350\241\250.md"
+++ "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\257\207/\351\223\276\350\241\250.md"
@@ -1,4 +1,4 @@
-## 链表
+ ## 链表
### 核心点
@@ -11,6 +11,79 @@
- 合并两个链表
- 找到链表的中间节点
+### 通解与模板
+
+#### 1. 核心解题思维
+
+- **虚拟头节点 (Dummy Node)**:
+ - **用途**: 统一处理头节点可能变化的情况(如删除、插入、合并)。
+ - **通解**: `let dummy = new ListNode(-1); dummy.next = head;` 最后返回 `dummy.next`。
+
+- **快慢指针 (Fast & Slow Pointers)**:
+ - **用途**: 处理距离、环、中点类问题。
+ - **场景**: 找中点(快2慢1)、判环、找倒数第 K 个节点。
+
+- **多指针协作**:
+ - **用途**: 链表无法随机访问,通常需要 `pre`, `cur`, `next` 配合完成断链、连链操作。
+
+#### 2. 常见代码模板
+
+**A. 虚拟头节点 (处理头节点变动)**
+
+```js
+var genericSolution = function(head) {
+ let dummy = new ListNode(-1); // 哨兵节点
+ dummy.next = head;
+ let pre = dummy;
+ let cur = head;
+ while (cur !== null) {
+ // 逻辑处理...
+ cur = cur.next;
+ }
+ return dummy.next;
+};
+```
+
+**B. 快慢指针 (找中点/判环)**
+
+```js
+var findMiddle = function(head) {
+ let slow = head;
+ let fast = head.next;
+ while (fast !== null && fast.next !== null) {
+ slow = slow.next;
+ fast = fast.next.next;
+ }
+ return slow;
+};
+```
+
+**C. 反转链表 (迭代法)**
+
+```js
+var reverseList = function(head) {
+ let prev = null;
+ let curr = head;
+ while (curr !== null) {
+ let nextTemp = curr.next;
+ curr.next = prev;
+ prev = curr;
+ curr = nextTemp;
+ }
+ return prev;
+};
+```
+
+#### 3. 常见题型策略
+
+| 题型分类 | 关键策略 |
+| :------------- | :---------------------------------------------------------------------- |
+| **基础操作** | **Dummy Node** 是神器。删除、插入操作都需要找到目标节点的**前驱节点**。 |
+| **反转类** | 熟练掌握 `pre`, `cur`, `next` 三指针迭代法。 |
+| **合并/排序** | **归并排序**思想。找中点 -> 断开 -> 递归排序 -> 合并有序链表。 |
+| **双指针技巧** | **快慢指针**找中点或判环;**双指针**分别遍历两个链表。 |
+| **重排/拼接** | 组合拳:通常涉及 **找中点 + 反转后半部分 + 合并链表** 的步骤。 |
+
**链表的数据结构**
```js
@@ -31,12 +104,12 @@ function ListNode(val) {
```js
var deleteDuplicates = function(head) {
- let current = head;
- while(current !== null && current.next !== null){
- if(current.val === current.next.val){
- current.next = current.next.next;
+ let cur = head;
+ while(cur !== null && cur.next !== null){
+ if(cur.val === cur.next.val){
+ cur.next = cur.next.next;
}else{
- current = current.next;
+ cur = cur.next;
}
}
return head;
@@ -59,27 +132,21 @@ var deleteDuplicates = function(head) { // 递归写法
```js
var deleteDuplicates = function(head) {
- if (head === null || head.next === null) {
- return head;
- }
let dummy = new ListNode(-1);
dummy.next = head;
- let front = dummy;
- let back = head.next;
- while(back !== null){
- if(front.next.val !== back.val){
- front = front.next;
- back = back.next;
- } else {
- while(back !== null && front.next.val === back.val){
- back = back.next;
+ let pre = dummy;
+ while (pre.next !== null && pre.next.next !== null) {
+ if (pre.next.val === pre.next.next.val) {
+ let val = pre.next.val;
+ while (pre.next !== null && pre.next.val === val) {
+ pre.next = pre.next.next;
}
- front.next = back;
- back = back === null ? null : back.next;
- }
+ } else {
+ pre = pre.next;
+ }
}
return dummy.next;
-};
+}
```
##### 206.反转一个单链表(头插法)。[reverse-linked-list](https://leetcode-cn.com/problems/reverse-linked-list/)
@@ -89,15 +156,15 @@ var deleteDuplicates = function(head) {
```js
var reverseList = function(head) {
- if(head === null || head.next === null) return head;
- let dummy = new ListNode(-1);
- while(head !== null){
- let p = head.next;
- head.next = dummy.next;
- dummy.next = head;
- head = p;
+ let prev = null;
+ let cur = head;
+ while (cur !== null) {
+ let next = cur.next;
+ cur.next = prev;
+ prev = cur;
+ cur = next;
}
- return dummy.next;
+ return prev;
};
```
@@ -111,17 +178,17 @@ var reverseList = function(head) {
```js
var reverseBetween = function(head, m, n) {
let dummy = new ListNode(-1);
- dummy.next = head; //哑巴节点
- head = dummy;
- for(let i=m;i>1;i--){
- head = head.next; // 让head指向反转子列表的前一个节点
- }
- let pre = head.next; //指向子列表的第一个节点
- for(let j=m;j {
- let slow = head
- let fast = head.next
+const mergeSort = head => {
+ if (head === null || head.next === null) {
+ return head;
+ }
+ let slow = head;
+ let fast = head.next.next;
while (fast !== null && fast.next !== null) {
- slow = slow.next
- fast = fast.next.next
+ slow = slow.next;
+ fast = fast.next.next;
}
- return slow
+ let mid = slow.next;
+ slow.next = null;
+ let left = mergeSort(head);
+ let right = mergeSort(mid);
+ return merge(left, right);
}
-const mergeTwoList = (l1, l2) => {
- const dummy = new ListNode(0)
- let p = dummy
+const merge = (l1, l2) => {
+ let dummy = new ListNode(-1);
+ let pre = dummy;
while (l1 !== null && l2 !== null) {
if (l1.val < l2.val) {
- p.next = l1
- l1 = l1.next
+ pre.next = l1;
+ l1 = l1.next;
} else {
- p.next = l2
- l2 = l2.next
+ pre.next = l2;
+ l2 = l2.next;
}
- p = p.next
- }
- if (l1 !== null) { // l1不为空,p指向剩下的链表
- p.next = l1
- }
- if (l2 !== null) {
- p.next = l2
+ pre = pre.next;
}
- return dummy.next
-}
-
-const mergeSort = head => {
- if (head === null || head.next === null) {
- return head
- }
- let mid = findMidNode(head)
- let tail = mid.next
- mid.next = null
- let left = mergeSort(head)
- let right = mergeSort(tail)
- return mergeTwoList(left, right)
+ pre.next = l1 !== null ? l1 : l2;
+ return dummy.next;
}
```
@@ -264,23 +319,36 @@ const mergeSort = head => {
你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
```js
-var reorderList = function (head) {
- const nodeArr = []
- let node = head
- while (node !== null) {
- nodeArr.push(node)
- node = node.next
- }
- let i = 0, j = nodeArr.length - 1
- while (i < j) {
- nodeArr[i].next = nodeArr[j]
- i++
- if (i === j) break
- nodeArr[j].next = nodeArr[i]
- j--
- }
- nodeArr[i].next = null
-}
+var reorderList = function(head) {
+ if (head === null) return;
+ let slow = head;
+ let fast = head;
+ while (fast !== null && fast.next !== null) {
+ slow = slow.next;
+ fast = fast.next.next;
+ }
+
+ let prev = null;
+ let cur = slow.next;
+ slow.next = null;
+ while (cur !== null) {
+ let next = cur.next;
+ cur.next = prev;
+ prev = cur;
+ cur = next;
+ }
+
+ let head1 = head;
+ let head2 = prev;
+ while (head1 !== null && head2 !== null) {
+ let next1 = head1.next;
+ let next2 = head2.next;
+ head1.next = head2;
+ head1 = next1;
+ head2.next = next1; // 修正逻辑:head1->head2->next1
+ head2 = next2;
+ }
+};
```
##### [141. 环形链表](https://leetcode-cn.com/problems/linked-list-cycle/)
@@ -288,21 +356,16 @@ var reorderList = function (head) {
给定一个链表,判断链表中是否有环。
```js
-var hasCycle = function (head) {
- if (head === null || head.next === null) {
- return false
- }
- let slow = head
- let fast = head.next
- while (fast !== null && fast.next !== null && slow !== null) {
- if (slow === fast) {
- return true
- }
- slow = slow.next
- fast = fast.next.next
+var hasCycle = function(head) {
+ let slow = head;
+ let fast = head;
+ while (fast !== null && fast.next !== null) {
+ slow = slow.next;
+ fast = fast.next.next;
+ if (slow === fast) return true;
}
- return false
-}
+ return false;
+};
```
##### [142. 环形链表 II](https://leetcode-cn.com/problems/linked-list-cycle-ii/)
@@ -310,40 +373,48 @@ var hasCycle = function (head) {
给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 `null`。
```js
-var detectCycle = function (head) {
- let visited = new Set() //使用哈希表,空间复杂度O(n)
- while (head !== null) {
- if(visited.has(head)){
- return head
+var detectCycle = function(head) {
+ let slow = head;
+ let fast = head;
+ while (fast !== null && fast.next !== null) {
+ slow = slow.next;
+ fast = fast.next.next;
+ if (slow === fast) {
+ slow = head;
+ while (slow !== fast) {
+ slow = slow.next;
+ fast = fast.next;
+ }
+ return slow;
}
- visited.add(head)
- head = head.next
}
- return null
+ return null;
};
```
+##### [160. 相交链表](https://leetcode-cn.com/problems/intersection-of-two-linked-lists/)
+
+给你两个单链表的头节点 `headA` 和 `headB` ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 `null` 。
+
+**核心思路:双指针(浪漫相遇)**
+- 指针 `pA` 遍历 `A`,遍历完后跳转到 `headB`。
+- 指针 `pB` 遍历 `B`,遍历完后跳转到 `headA`。
+- 两个指针走过的总路程相同(`lenA + lenB`),如果相交,它们必会在交点处相遇;如果不相交,它们必会同时指向 `null`。
+
```js
-var detectCycle = function (head) {
- if (head === null || head.next === null) {
- return null
- }
- let slow = head
- let fast = head.next
- while(fast !== null && fast.next !== null){
- if(fast === slow) { //数学推导,看官网题解
- slow = slow.next
- fast = head
- while(fast !== slow){
- slow = slow.next
- fast = fast.next
- }
- return slow
- }
- slow = slow.next
- fast = fast.next.next
+var getIntersectionNode = function(headA, headB) {
+ if (headA === null || headB === null) return null;
+ let pA = headA;
+ let pB = headB;
+
+ // 只要不相等就继续走
+ while (pA !== pB) {
+ // 走完 A 就走 B,走完 B 就走 A
+ pA = pA === null ? headB : pA.next;
+ pB = pB === null ? headA : pB.next;
}
- return null
+
+ return pA;
};
```
@@ -352,42 +423,32 @@ var detectCycle = function (head) {
请判断一个链表是否为回文链表。
```js
-var isPalindrome = function (head) {
- if (head === null || head.next === null) {
- return true
- }
- let slow = head
- let fast = head.next
+var isPalindrome = function(head) {
+ let slow = head;
+ let fast = head;
while (fast !== null && fast.next !== null) {
- slow = slow.next
- fast = fast.next.next
- }
- let tail = reverseList(slow.next)
- slow.next = null
- while (head !== null && tail !== null) {
- if (head.val !== tail.val) {
- return false
- }
- head = head.next
- tail = tail.next
+ slow = slow.next;
+ fast = fast.next.next;
}
- return true
-};
-
-const reverseList = head => {
- if (head === null) {
- return head
+
+ let prev = null;
+ let cur = slow;
+ while (cur !== null) {
+ let next = cur.next;
+ cur.next = prev;
+ prev = cur;
+ cur = next;
}
- const dummy = new ListNode(0)
- let prev = head
- while (prev !== null) { //头插法
- let temp = prev
- prev = prev.next
- temp.next = dummy.next
- dummy.next = temp
+
+ let p1 = head;
+ let p2 = prev;
+ while (p2 !== null) {
+ if (p1.val !== p2.val) return false;
+ p1 = p1.next;
+ p2 = p2.next;
}
- return dummy.next
-}
+ return true;
+};
```
##### [138. 复制带随机指针的链表](https://leetcode-cn.com/problems/copy-list-with-random-pointer/)
@@ -395,37 +456,213 @@ const reverseList = head => {
给你一个长度为 `n` 的链表,每个节点包含一个额外增加的随机指针 `random` ,该指针可以指向链表中的任何节点或空节点。
```js
-var copyRandomList = function (head) {
- if (head === null) {
- return head
- }
- let cur = head
+var copyRandomList = function(head) {
+ if (head === null) return null;
+ let cur = head;
while (cur !== null) {
- const clone = new Node(cur.val, cur.next, cur.random)
- const temp = cur.next
- cur.next = clone
- cur = temp
+ let clone = new Node(cur.val, cur.next, null);
+ cur.next = clone;
+ cur = clone.next;
}
- cur = head
+ cur = head;
while (cur !== null) {
if (cur.random !== null) {
- cur.next.random = cur.random.next
+ cur.next.random = cur.random.next;
}
- cur = cur.next.next
+ cur = cur.next.next;
}
- cur = head
- let cloneHead = cur.next
- while (cur !== null && cur.next !== null) {
- const temp = cur.next
- cur.next = cur.next.next
- cur = temp
+ cur = head;
+ let cloneHead = cur.next;
+ while (cur.next !== null) {
+ let temp = cur.next;
+ cur.next = cur.next.next;
+ cur = temp;
}
- return cloneHead
+ return cloneHead;
};
```
+
+##### [23. 合并 K 个升序链表](https://leetcode-cn.com/problems/merge-k-sorted-lists/)
+
+给你一个链表数组,每个链表都已经按升序排列。请你将所有链表合并到一个升序链表中,返回合并后的链表。
+
+**方式一:分治法(归并思想)**
+
+```js
+var mergeKLists = function(lists) {
+ if (lists.length === 0) return null;
+ return solve(lists, 0, lists.length - 1);
+};
+
+function solve(lists, left, right) {
+ if (left === right) return lists[left];
+ let mid = Math.floor((left + right) / 2);
+ let l1 = solve(lists, left, mid);
+ let l2 = solve(lists, mid + 1, right);
+ return mergeTwoLists(l1, l2);
+}
+
+function mergeTwoLists(l1, l2) {
+ let dummy = new ListNode(-1);
+ let pre = dummy;
+ while (l1 && l2) {
+ if (l1.val < l2.val) {
+ pre.next = l1;
+ l1 = l1.next;
+ } else {
+ pre.next = l2;
+ l2 = l2.next;
+ }
+ pre = pre.next;
+ }
+ pre.next = l1 || l2;
+ return dummy.next;
+}
+```
+
+**方式二:优先级队列(最小堆)**
+
+> JS 需手写堆,面试中建议优先使用分治法。
+
+```js
+var mergeKLists = function(lists) {
+ let dummy = new ListNode(-1);
+ let p = dummy;
+ let pq = new MinHeap((a, b) => a.val < b.val); // 伪代码:假设有最小堆
+
+ for (let head of lists) {
+ if (head) pq.push(head);
+ }
+
+ while (!pq.isEmpty()) {
+ let node = pq.pop();
+ p.next = node;
+ if (node.next) pq.push(node.next);
+ p = p.next;
+ }
+ return dummy.next;
+};
+```
+
+##### [25. K 个一组翻转链表](https://leetcode-cn.com/problems/reverse-nodes-in-k-group/)
+
+给你链表的头节点 `head` ,每 `k` 个节点一组进行翻转,请你返回修改后的链表。`k` 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 `k` 的整数倍,那么请将最后剩余的节点保持原有顺序。
+
+```js
+var reverseKGroup = function(head, k) {
+ let cur = head;
+ let count = 0;
+ // 探测是否够 k 个
+ while (cur !== null && count !== k) {
+ cur = cur.next;
+ count++;
+ }
+ if (count === k) {
+ // 反转这 k 个节点
+ let prev = null;
+ let node = head;
+ for (let i = 0; i < k; i++) {
+ let next = node.next;
+ node.next = prev;
+ prev = node;
+ node = next;
+ }
+ // 递归连接
+ head.next = reverseKGroup(cur, k);
+ return prev;
+ }
+ return head;
+};
+```
+
+##### [146. LRU 缓存](https://leetcode-cn.com/problems/lru-cache/)
+
+设计并实现一个满足 [LRU (最近最少使用) 缓存](https://baike.baidu.com/item/LRU) 约束的数据结构。
+
+**核心思想**:
+- **哈希表 (Map)**:用于 $O(1)$ 时间复杂度通过 `key` 快速定位节点。
+- **双向链表 (Doubly Linked List)**:用于 $O(1)$ 时间复杂度增加/删除节点,并维护访问顺序。
+ - 靠近 **虚拟头节点 (head)** 的是最近访问的。
+ - 靠近 **虚拟尾节点 (tail)** 的是最久未访问的。
+
+```javascript
+/**
+ * @param {number} capacity
+ */
+var LRUCache = function(capacity) {
+ this.capacity = capacity;
+ this.map = new Map(); // key -> node
+ this.head = new ListNode(-1, -1); // 虚拟头
+ this.tail = new ListNode(-1, -1); // 虚拟尾
+ this.head.next = this.tail;
+ this.tail.prev = this.head;
+};
+
+function ListNode(key, val) {
+ this.key = key;
+ this.val = val;
+ this.prev = null;
+ this.next = null;
+}
+
+/**
+ * @param {number} key
+ * @return {number}
+ */
+LRUCache.prototype.get = function(key) {
+ if (!this.map.has(key)) return -1;
+ const node = this.map.get(key);
+ this.moveToHead(node); // 访问后移到头部
+ return node.val;
+};
+
+/**
+ * @param {number} key
+ * @param {number} value
+ * @return {void}
+ */
+LRUCache.prototype.put = function(key, value) {
+ if (this.map.has(key)) {
+ const node = this.map.get(key);
+ node.val = value;
+ this.moveToHead(node);
+ } else {
+ if (this.map.size === this.capacity) {
+ const lastNode = this.tail.prev;
+ this.removeNode(lastNode);
+ this.map.delete(lastNode.key);
+ }
+ const newNode = new ListNode(key, value);
+ this.addNodeToHead(newNode);
+ this.map.set(key, newNode);
+ }
+};
+
+// 辅助函数:将节点移到头部
+LRUCache.prototype.moveToHead = function(node) {
+ this.removeNode(node);
+ this.addNodeToHead(node);
+};
+
+// 辅助函数:删除节点
+LRUCache.prototype.removeNode = function(node) {
+ node.prev.next = node.next;
+ node.next.prev = node.prev;
+};
+
+// 辅助函数:在头部插入节点
+LRUCache.prototype.addNodeToHead = function(node) {
+ node.next = this.head.next;
+ node.prev = this.head;
+ this.head.next.prev = node;
+ this.head.next = node;
+};
+```
+
## 练习
+
- [remove-duplicates-from-sorted-list](https://leetcode-cn.com/problems/remove-duplicates-from-sorted-list/)
- [remove-duplicates-from-sorted-list-ii](https://leetcode-cn.com/problems/remove-duplicates-from-sorted-list-ii/)
- [reverse-linked-list](https://leetcode-cn.com/problems/reverse-linked-list/)
@@ -437,4 +674,6 @@ var copyRandomList = function (head) {
- [linked-list-cycle](https://leetcode-cn.com/problems/linked-list-cycle/)
- [linked-list-cycle-ii](https://leetcode-cn.com/problems/linked-list-cycle-ii/)
- [palindrome-linked-list](https://leetcode-cn.com/problems/palindrome-linked-list/)
-- [copy-list-with-random-pointer](https://leetcode-cn.com/problems/copy-list-with-random-pointer/)
\ No newline at end of file
+- [copy-list-with-random-pointer](https://leetcode-cn.com/problems/copy-list-with-random-pointer/)
+- [reverse-nodes-in-k-group](https://leetcode-cn.com/problems/reverse-nodes-in-k-group/)
+- [merge-k-sorted-lists](https://leetcode-cn.com/problems/merge-k-sorted-lists/)
diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264/\345\217\214\346\214\207\351\222\210\344\270\216\346\273\221\345\212\250\347\252\227\345\217\243.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264/\345\217\214\346\214\207\351\222\210\344\270\216\346\273\221\345\212\250\347\252\227\345\217\243.md"
new file mode 100644
index 0000000..17e4a88
--- /dev/null
+++ "b/\347\256\227\346\263\225\346\200\235\347\273\264/\345\217\214\346\214\207\351\222\210\344\270\216\346\273\221\345\212\250\347\252\227\345\217\243.md"
@@ -0,0 +1,519 @@
+# 双指针与滑动窗口 (Two Pointers & Sliding Window)
+
+## 核心哲学
+双指针技巧的本质是**利用数组的单调性(有序性)或问题的连续性**,通过两个指针的移动,将原本需要两层嵌套循环 ($O(N^2)$) 的搜索空间,优化为单次遍历 ($O(N)$)。
+
+---
+
+## 一、 通解与模板:三大思维模式
+
+### 1. 左右指针 (对撞指针 / Opposite Pointers)
+**核心思想**:指针从两端向中间汇合。
+**适用场景**:有序数组搜索、反转、回文判断、接雨水。
+
+**口诀**:
+1. **排序是前提**(搜索类题目)。
+2. **双向奔赴**:根据当前和与目标值的关系,移动左或右指针。
+
+```js
+// 左右指针通用模板
+var oppositePointers = function(nums) {
+ let left = 0, right = nums.length - 1;
+ while (left < right) {
+ let sum = nums[left] + nums[right];
+ if (sum === target) {
+ return [left, right];
+ } else if (sum < target) {
+ left++; // 让 sum 变大
+ } else {
+ right--; // 让 sum 变小
+ }
+ }
+};
+```
+
+### 2. 快慢指针 (同向指针 / Fast-Slow Pointers)
+**核心思想**:两个指针同向移动,速度不同或起点不同。
+**适用场景**:
+- **数组**:原地修改(去重、移动元素)。
+- **链表**:找中点、判断环、找倒数第 K 个。
+
+```js
+// 快慢指针模板 (原地修改数组)
+var fastSlowPointers = function(nums) {
+ let slow = 0, fast = 0;
+ while (fast < nums.length) {
+ // 这里的条件通常是:nums[fast] 是否是一个需要保留的“合格”元素
+ if (/* nums[fast] 满足条件 */) {
+ nums[slow] = nums[fast];
+ slow++;
+ }
+ fast++;
+ }
+ return slow; // 返回新长度
+};
+```
+
+### 3. 滑动窗口 (Sliding Window - 最强模板)
+**核心思想**:维护一个“窗口”,通过扩大和缩小窗口来寻找满足条件的子区间。
+**适用场景**:子串匹配、连续子数组和/长度。
+
+**万能模板 (背诵全文)**:
+```js
+/* 滑动窗口万能模板 */
+var slidingWindow = function(s, t) {
+ // 1. 初始化窗口及其辅助数据结构
+ let need = new Map(); // 我们需要的字符及其频率
+ let window = new Map(); // 窗口中当前的字符及其频率
+ for (let c of t) need.set(c, (need.get(c) || 0) + 1);
+
+ let left = 0, right = 0;
+ let valid = 0; // 窗口中满足 need 条件的字符种类数
+ let res = /* 根据题目要求初始化 */;
+
+ while (right < s.length) {
+ // a. 【入窗】:c 是将要移入窗口的字符
+ let c = s[right];
+ right++; // 右移窗口
+
+ // --- 进行窗口内数据的一系列更新 ---
+ if (need.has(c)) {
+ window.set(c, (window.get(c) || 0) + 1);
+ if (window.get(c) === need.get(c)) valid++;
+ }
+
+ // b. 【收缩】:判断左侧窗口是否要收缩
+ // 对于求“最小”窗口:当满足条件时收缩,以寻找更短的可能
+ // 对于求“最大”窗口:当不满足限制时收缩,以恢复合法状态
+ while (/* window needs shrink */) {
+ // d 是将要移出窗口的字符
+ let d = s[left];
+ left++; // 左移窗口
+
+ // --- 进行窗口内数据的一系列更新 ---
+ if (need.has(d)) {
+ if (window.get(d) === need.get(d)) valid--;
+ window.set(d, window.get(d) - 1);
+ }
+ }
+
+ // c. 【更新结果】:根据题目要求在合适的位置更新 res
+ // 如果求最大长度,通常在 while 外更新:res = Math.max(res, right - left)
+ // 如果求最小长度,通常在 while 内更新:res = Math.min(res, right - left)
+ }
+ return res;
+};
+```
+
+---
+
+## 二、 记忆心法总结
+
+1. **看到“有序数组” + “目标值”** -> 优先想 **左右指针**。
+2. **看到“连续子数组/子串” + “最长/最短/计数”** -> 优先想 **滑动窗口**。
+3. **看到“原地修改”或“链表环”** -> 优先想 **快慢指针**。
+4. **滑动窗口四问**:
+ - 什么时候应该扩大窗口 (`right++`)?
+ - 什么时候应该缩小窗口 (`left++`)?
+ - 扩大/缩小窗口时,应该更新哪些数据?
+ - 我们要的结果是在扩大时更新,还是在缩小时更新?
+
+---
+
+## 三、 经典题型深度解析
+
+### 1. 三数之和 (15. 3Sum)
+**核心**:排序 + 固定一个数 + 左右指针。
+**难点**:去重。
+
+```js
+var threeSum = function(nums) {
+ nums.sort((a, b) => a - b);
+ const res = [];
+ for (let i = 0; i < nums.length - 2; i++) {
+ if (nums[i] > 0) break; // 优化:第一个数大于0,和不可能为0
+ if (i > 0 && nums[i] === nums[i-1]) continue; // 去重第一个数
+
+ let l = i + 1, r = nums.length - 1;
+ while (l < r) {
+ let sum = nums[i] + nums[l] + nums[r];
+ if (sum === 0) {
+ res.push([nums[i], nums[l], nums[r]]);
+ while (l < r && nums[l] === nums[l+1]) l++; // 去重左指针
+ while (l < r && nums[r] === nums[r-1]) r--; // 去重右指针
+ l++; r--;
+ } else if (sum < 0) l++;
+ else r--;
+ }
+ }
+ return res;
+};
+```
+
+### 2. 无重复字符的最长子串 (3. Longest Substring Without Repeating Characters)
+**核心**:滑动窗口基础题。
+**思路**:维护一个窗口,保证窗口内没有重复字符。当新加入的字符导致重复时,收缩左边界直到重复消除。
+
+```js
+var lengthOfLongestSubstring = function(s) {
+ let window = new Map();
+ let left = 0, right = 0;
+ let res = 0;
+
+ while (right < s.length) {
+ let c = s[right];
+ right++;
+ // 进行窗口内数据的一系列更新
+ window.set(c, (window.get(c) || 0) + 1);
+
+ // 判断左侧窗口是否要收缩
+ while (window.get(c) > 1) {
+ let d = s[left];
+ left++;
+ // 进行窗口内数据的一系列更新
+ window.set(d, window.get(d) - 1);
+ }
+
+ // 更新结果
+ res = Math.max(res, right - left);
+ }
+ return res;
+};
+```
+
+### 3. 长度最小的子数组 (209. Minimum Size Subarray Sum)
+**核心**:滑动窗口求最小窗口。
+**思路**:窗口和小于 target 时右移扩大,大于等于 target 时左移缩小并更新最小长度。
+
+```js
+var minSubArrayLen = function(target, nums) {
+ let left = 0, right = 0;
+ let sum = 0;
+ let res = Infinity;
+
+ while (right < nums.length) {
+ let c = nums[right];
+ right++;
+ // 窗口内数据更新
+ sum += c;
+
+ // 收缩窗口条件:sum >= target
+ while (sum >= target) {
+ // 更新结果:在收缩前/后均可,这里是满足条件即更新
+ res = Math.min(res, right - left);
+
+ let d = nums[left];
+ left++;
+ // 窗口内数据更新
+ sum -= d;
+ }
+ }
+ return res === Infinity ? 0 : res;
+};
+```
+
+### 4. 最小覆盖子串 (76. Minimum Window Substring)
+**核心**:滑动窗口模板的完美体现。
+
+```js
+var minWindow = function(s, t) {
+ let need = new Map(), window = new Map();
+ for (let c of t) need.set(c, (need.get(c) || 0) + 1);
+
+ let left = 0, right = 0, valid = 0;
+ let start = 0, len = Infinity; // 记录最小覆盖子串的起点和长度
+
+ while (right < s.length) {
+ let c = s[right++];
+ if (need.has(c)) {
+ window.set(c, (window.get(c) || 0) + 1);
+ if (window.get(c) === need.get(c)) valid++;
+ }
+
+ while (valid === need.size) {
+ if (right - left < len) {
+ start = left;
+ len = right - left;
+ }
+ let d = s[left++];
+ if (need.has(d)) {
+ if (window.get(d) === need.get(d)) valid--;
+ window.set(d, window.get(d) - 1);
+ }
+ }
+ }
+ return len === Infinity ? "" : s.substr(start, len);
+};
+```
+
+### 5. 滑动窗口最大值 (239. Sliding Window Maximum)
+**核心**:单调队列 (Monotonic Queue) + 滑动窗口。
+**思路**:维护一个单调递减队列,队列头部始终是当前窗口的最大值下标。
+
+```js
+var maxSlidingWindow = function(nums, k) {
+ let deque = []; // 存下标,对应值单调递减
+ let res = [];
+
+ for (let i = 0; i < nums.length; i++) {
+ // 1. 入队:保持单调递减,移除比当前元素小的队尾元素
+ while (deque.length && nums[deque[deque.length - 1]] < nums[i]) {
+ deque.pop();
+ }
+ deque.push(i);
+
+ // 2. 出队:判断队头是否滑出窗口
+ if (deque[0] < i - k + 1) {
+ deque.shift();
+ }
+
+ // 3. 记录结果:当窗口形成时 (i >= k - 1)
+ if (i >= k - 1) {
+ res.push(nums[deque[0]]);
+ }
+ }
+ return res;
+};
+```
+
+### 6. 接雨水 (42. Trapping Rain Water)
+**核心**:双指针极致优化空间。
+
+```js
+var trap = function(height) {
+ let left = 0, right = height.length - 1;
+ let l_max = 0, r_max = 0;
+ let res = 0;
+
+ while (left < right) {
+ l_max = Math.max(l_max, height[left]);
+ r_max = Math.max(r_max, height[right]);
+
+ // 核心逻辑:木桶效应,水的高度取决于较低的那一边
+ if (l_max < r_max) {
+ res += l_max - height[left];
+ left++;
+ } else {
+ res += r_max - height[right];
+ right--;
+ }
+ }
+ return res;
+};
+```
+
+---
+
+## 五、 广义双指针与数组技巧拓展
+
+### 1. 贪心策略 (Greedy Strategy)
+**121. 买卖股票的最佳时机 (Best Time to Buy and Sell Stock)**
+**核心**:一次遍历。记录历史最低点,计算当前卖出的最大利润。
+
+```js
+var maxProfit = function(prices) {
+ let minPrice = Infinity;
+ let maxProfit = 0;
+ for (let price of prices) {
+ if (price < minPrice) {
+ minPrice = price;
+ } else if (price - minPrice > maxProfit) {
+ maxProfit = price - minPrice;
+ }
+ }
+ return maxProfit;
+};
+```
+
+**122. 买卖股票的最佳时机 II (Best Time to Buy and Sell Stock II)**
+**核心**:贪心算法。只要今天比昨天高,就交易(收集所有正利润)。
+
+```js
+var maxProfit = function(prices) {
+ let profit = 0;
+ for (let i = 1; i < prices.length; i++) {
+ if (prices[i] > prices[i-1]) {
+ profit += prices[i] - prices[i-1];
+ }
+ }
+ return profit;
+};
+```
+
+### 2. 逆向与排列双指针 (Reverse & Permutation Pointers)
+**88. 合并两个有序数组 (Merge Sorted Array)**
+**核心**:逆向双指针。从后往前填,避免覆盖未处理的元素。
+
+```js
+var merge = function(nums1, m, nums2, n) {
+ let p1 = m - 1;
+ let p2 = n - 1;
+ let p = m + n - 1;
+
+ while (p1 >= 0 && p2 >= 0) {
+ if (nums1[p1] > nums2[p2]) {
+ nums1[p] = nums1[p1];
+ p1--;
+ } else {
+ nums1[p] = nums2[p2];
+ p2--;
+ }
+ p--;
+ }
+ // 如果 nums2 还有剩余,直接拷贝 (nums1 剩余不用管,本来就在原地)
+ while (p2 >= 0) {
+ nums1[p] = nums2[p2];
+ p2--;
+ p--;
+ }
+};
+```
+
+**31. 下一个排列 (Next Permutation)**
+**核心**:双指针寻找逆序对 + 反转。
+**步骤**:
+1. 从后往前找第一个升序对 `(i, i+1)`,满足 `nums[i] < nums[i+1]`。
+2. 从后往前找第一个大于 `nums[i]` 的数 `nums[j]`。
+3. 交换 `nums[i]` 和 `nums[j]`。
+4. 反转 `i+1` 之后的所有元素。
+
+```js
+var nextPermutation = function(nums) {
+ let i = nums.length - 2;
+ while (i >= 0 && nums[i] >= nums[i+1]) i--; // 找第一个小
+
+ if (i >= 0) {
+ let j = nums.length - 1;
+ while (j >= 0 && nums[j] <= nums[i]) j--; // 找第一个大
+ [nums[i], nums[j]] = [nums[j], nums[i]]; // 交换
+ }
+
+ // 反转 i 之后的部分
+ let left = i + 1, right = nums.length - 1;
+ while (left < right) {
+ [nums[left], nums[right]] = [nums[right], nums[left]];
+ left++; right--;
+ }
+};
+```
+
+### 3. 二维矩阵模拟 (Matrix Simulation)
+**54. 螺旋矩阵 (Spiral Matrix)**
+**核心**:模拟。设定上下左右四个边界,按顺序遍历并收缩边界。
+
+```js
+var spiralOrder = function(matrix) {
+ if (!matrix.length) return [];
+ let l = 0, r = matrix[0].length - 1, t = 0, b = matrix.length - 1;
+ let res = [];
+
+ while (true) {
+ for (let i = l; i <= r; i++) res.push(matrix[t][i]); // left to right
+ if (++t > b) break;
+ for (let i = t; i <= b; i++) res.push(matrix[i][r]); // top to bottom
+ if (--r < l) break;
+ for (let i = r; i >= l; i--) res.push(matrix[b][i]); // right to left
+ if (--b < t) break;
+ for (let i = b; i >= t; i--) res.push(matrix[i][l]); // bottom to top
+ if (++l > r) break;
+ }
+ return res;
+};
+```
+
+**48. 旋转图像 (Rotate Image)**
+**核心**:数学规律。先水平翻转(上下对称交换),再主对角线翻转(转置)。
+**规律**:`matrix[i][j]` -> `matrix[j][n-1-i]`。
+
+```js
+var rotate = function(matrix) {
+ const n = matrix.length;
+ // 1. 水平翻转
+ for (let i = 0; i < Math.floor(n / 2); i++) {
+ for (let j = 0; j < n; j++) {
+ [matrix[i][j], matrix[n-1-i][j]] = [matrix[n-1-i][j], matrix[i][j]];
+ }
+ }
+ // 2. 主对角线翻转
+ for (let i = 0; i < n; i++) {
+ for (let j = 0; j < i; j++) {
+ [matrix[i][j], matrix[j][i]] = [matrix[j][i], matrix[i][j]];
+ }
+ }
+};
+```
+
+### 4. 区间与原地哈希 (Intervals & Cyclic Sort)
+**56. 合并区间 (Merge Intervals)**
+**核心**:排序 + 贪心。按起点排序,维护一个当前合并区间。
+
+```js
+var merge = function(intervals) {
+ if (!intervals.length) return [];
+ intervals.sort((a, b) => a[0] - b[0]);
+
+ let res = [intervals[0]];
+ for (let i = 1; i < intervals.length; i++) {
+ let curr = intervals[i];
+ let last = res[res.length - 1];
+
+ if (curr[0] <= last[1]) {
+ // 有重叠,合并(更新终点)
+ last[1] = Math.max(last[1], curr[1]);
+ } else {
+ // 无重叠,加入新区间
+ res.push(curr);
+ }
+ }
+ return res;
+};
+```
+
+**41. 缺失的第一个正数 (First Missing Positive)**
+**核心**:原地 Hash (Cyclic Sort)。将数字 `x` 放在下标 `x-1` 的位置上。
+**规则**:`nums[i]` 应该放在 `nums[i] - 1` 的位置。
+
+```js
+var firstMissingPositive = function(nums) {
+ let n = nums.length;
+ for (let i = 0; i < n; i++) {
+ // 1. 也就是 nums[i] 在 [1, n] 范围内
+ // 2. 且 nums[i] 没有放在正确的位置 (nums[i] != nums[nums[i]-1])
+ while (nums[i] > 0 && nums[i] <= n && nums[i] !== nums[nums[i]-1]) {
+ // 交换到正确位置
+ let targetIndex = nums[i] - 1;
+ [nums[i], nums[targetIndex]] = [nums[targetIndex], nums[i]];
+ }
+ }
+
+ // 再次遍历,找第一个不匹配的
+ for (let i = 0; i < n; i++) {
+ if (nums[i] !== i + 1) return i + 1;
+ }
+ return n + 1;
+};
+```
+
+---
+
+## 四、 拓展:链表中的双指针
+
+
+1. **判断环 (Linked List Cycle)**:快慢指针,快走二慢走一,相遇则有环。
+2. **找中点 (Middle of Linked List)**:快慢指针,快走完时慢在中点。
+3. **找倒数第 K 个 (Remove Nth Node From End)**:快指针先走 K 步,然后同步走。
+
+```js
+// 找倒数第 n 个节点
+var getKthFromEnd = function(head, n) {
+ let fast = head, slow = head;
+ // 快指针先走 n 步
+ while (n--) fast = fast.next;
+ // 同步走,快指针到头时,慢指针就在倒数第 n 个
+ while (fast) {
+ fast = fast.next;
+ slow = slow.next;
+ }
+ return slow;
+};
+```
diff --git "a/\351\224\231\351\242\230\346\234\254.md" "b/\351\224\231\351\242\230\346\234\254.md"
new file mode 100644
index 0000000..24e7098
--- /dev/null
+++ "b/\351\224\231\351\242\230\346\234\254.md"
@@ -0,0 +1,85 @@
+# 算法错题本
+
+记录在刷题过程中遇到的典型错误、逻辑漏洞及 JavaScript 语言特性导致的坑。
+
+---
+
+## [23. 合并 K 个升序链表](https://leetcode-cn.com/problems/merge-k-sorted-lists/)
+
+### ❌ 错误代码片段 (分治法)
+
+```javascript
+var solve = (lists, left, right) => {
+ if(left === right) return lists[left];
+ let mid = (left + right) / 2; // 错误 1
+ let l1 = solve(lists, left, mid - 1); // 错误 2
+ let l2 = solve(lists, mid + 1, right);
+ return mergeLists(l1, l2);
+}
+
+var mergeLists = (list1, list2) => {
+ if(list1 === null || list2 === null) return null; // 错误 3
+ let dummy = new ListNode(-1);
+ let prev = dummy;
+ while(list1 || list2){ // 错误 4
+ if(list1.val < list2.val){
+ // ...
+ }
+ }
+}
+```
+
+### 🔍 错误分析
+
+#### 1. JavaScript 数值除法不自动取整
+* **现象**:`let mid = (left + right) / 2;` 在 `left=0, right=1` 时结果是 `0.5`。
+* **后果**:`lists[0.5]` 为 `undefined`,且递归区间计算会陷入混乱或死循环。
+* **教训**:JS 中做索引计算务必使用 `Math.floor()` 或 `~~` (按位非) 取整。
+
+#### 2. 二分递归区间漏掉中点
+* **现象**:划分区间为 `[left, mid - 1]` 和 `[mid + 1, right]`。
+* **后果**:索引为 `mid` 的那个链表直接被丢弃了,结果不完整。
+* **正确做法**:标准二分应为 `[left, mid]` 和 `[mid + 1, right]`。
+
+#### 3. 合并链表基准情况处理错误
+* **现象**:`if(list1 === null || list2 === null) return null;`
+* **后果**:如果 A 链表有值,B 链表为空,本应返回 A,结果返回了 `null`,导致数据丢失。
+* **正确做法**:`if (!list1) return list2; if (!list2) return list1;`
+
+#### 4. 循环条件与空指针访问
+* **现象**:`while(list1 || list2)` 配合内部 `list1.val`。
+* **后果**:当 `list1` 为空但 `list2` 还有值时,循环继续,执行 `list1.val` 会抛出 `TypeError: Cannot read property 'val' of null`。
+* **正确做法**:使用 `while(list1 && list2)` 只处理公共部分,循环结束后一次性连接剩余尾部:`prev.next = list1 || list2;`
+
+---
+
+### ✅ 正确实现
+
+```javascript
+var solve = (lists, left, right) => {
+ if (left === right) return lists[left];
+ let mid = Math.floor((left + right) / 2); // 1. 取整
+ let l1 = solve(lists, left, mid); // 2. 包含 mid
+ let l2 = solve(lists, mid + 1, right);
+ return mergeLists(l1, l2);
+}
+
+var mergeLists = (l1, l2) => {
+ if (!l1) return l2; // 3. 妥善处理空链表
+ if (!l2) return l1;
+ let dummy = new ListNode(-1);
+ let cur = dummy;
+ while (l1 && l2) { // 4. 只有两者都不为空才比较
+ if (l1.val < l2.val) {
+ cur.next = l1;
+ l1 = l1.next;
+ } else {
+ cur.next = l2;
+ l2 = l2.next;
+ }
+ cur = cur.next;
+ }
+ cur.next = l1 || l2; // 5. 连接剩余尾部
+ return dummy.next;
+}
+```