From 1dd2f7ad833e48d931fe40667057e28ecd761f02 Mon Sep 17 00:00:00 2001 From: maopb Date: Mon, 29 Dec 2025 18:23:04 +0800 Subject: [PATCH 01/17] =?UTF-8?q?feat(=E6=95=B0=E6=8D=AE=E7=BB=93=E6=9E=84?= =?UTF-8?q?=E7=AF=87):=20=E6=B7=BB=E5=8A=A0=E6=8B=93=E6=89=91=E6=8E=92?= =?UTF-8?q?=E5=BA=8F=E5=92=8C=E4=BC=98=E5=8C=96=E9=93=BE=E8=A1=A8=E5=88=A0?= =?UTF-8?q?=E9=99=A4=E9=87=8D=E5=A4=8D=E8=8A=82=E7=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加拓扑排序章节,包含BFS(Kahn算法)和DFS(三色标记)两种方法 - 添加LeetCode 207课程表问题的两种解法实现 - 添加LeetCode 210课程表II问题的解法实现 - 优化链表删除重复节点代码,改进算法逻辑和代码风格 - 更新知识点总结,补充拓扑排序的核心概念 - 添加课程表相关练习题链接到练习列表 --- ...10\345\222\214\351\230\237\345\210\227.md" | 116 +++++++++++++++++- .../\351\223\276\350\241\250.md" | 37 +++--- 2 files changed, 133 insertions(+), 20 deletions(-) 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..d5da87a 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" @@ -59,27 +59,26 @@ 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 dummy = new ListNode(-1) + dummy.next = head + let pre = dummy // pre 指向已确认不重复的最后一个节点 + + while (pre.next !== null && pre.next.next !== null) { + if (pre.next.val === pre.next.next.val) { + // 发现重复,记录重复值 + let dupVal = pre.next.val + // 跳过所有重复节点 + while (pre.next !== null && pre.next.val === dupVal) { + pre.next = pre.next.next } - front.next = back; - back = back === null ? null : back.next; - } + } else { + // 不重复,pre 前进 + pre = pre.next + } } - return dummy.next; -}; + + return dummy.next +} ``` ##### 206.反转一个单链表(头插法)。[reverse-linked-list](https://leetcode-cn.com/problems/reverse-linked-list/) From b408f5eaa1baa2ec9e032d30a6af80ec03ae1937 Mon Sep 17 00:00:00 2001 From: mpbfx <1835886801@qq.com> Date: Mon, 29 Dec 2025 23:23:55 +0800 Subject: [PATCH 02/17] =?UTF-8?q?feat:=20=E7=BB=9F=E4=B8=80=E9=93=BE?= =?UTF-8?q?=E8=A1=A8=E6=A8=A1=E7=89=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../\351\223\276\350\241\250.md" | 437 ++++++++++-------- 1 file changed, 235 insertions(+), 202 deletions(-) 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 d5da87a..0b7bcb6 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" @@ -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,25 +132,20 @@ var deleteDuplicates = function(head) { // 递归写法 ```js var deleteDuplicates = function(head) { - let dummy = new ListNode(-1) - dummy.next = head - let pre = dummy // pre 指向已确认不重复的最后一个节点 - + 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 dupVal = pre.next.val - // 跳过所有重复节点 - while (pre.next !== null && pre.next.val === dupVal) { - pre.next = pre.next.next + let val = pre.next.val; + while (pre.next !== null && pre.next.val === val) { + pre.next = pre.next.next; } } else { - // 不重复,pre 前进 - pre = pre.next + pre = pre.next; } } - - return dummy.next + return dummy.next; } ``` @@ -88,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; }; ``` @@ -110,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指向反转子列表的前一个节点 + dummy.next = head; + let pre = dummy; + for (let i = 1; i < m; i++) { + pre = pre.next; } - 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 - } - return dummy.next -} - -const mergeSort = head => { - if (head === null || head.next === null) { - return head + pre = pre.next; } - 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; } ``` @@ -263,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 +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 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-- + + let prev = null; + let cur = slow.next; + slow.next = null; + while (cur !== null) { + let next = cur.next; + cur.next = prev; + prev = cur; + cur = next; } - nodeArr[i].next = null -} + + 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/) @@ -287,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/) @@ -309,40 +373,22 @@ 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 - } - visited.add(head) - head = head.next - } - return 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 +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 slow; } - slow = slow.next - fast = fast.next.next } - return null + return null; }; ``` @@ -351,42 +397,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 + 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 - } - 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/) @@ -394,32 +430,29 @@ 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; }; ``` From a97f1e518705771b622235771550e19a8b76bb83 Mon Sep 17 00:00:00 2001 From: maopb Date: Wed, 31 Dec 2025 18:03:54 +0800 Subject: [PATCH 03/17] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20LeetCode=20?= =?UTF-8?q?=E8=A7=84=E5=88=99=E6=96=87=E4=BB=B6=E5=B9=B6=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=20reverseKGroup=20=E7=AE=97=E6=B3=95=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .agent/rules/leetcode.md | 23 ++++ test.js | 21 ++++ .../\351\223\276\350\241\250.md" | 113 ++++++++++++++++-- 3 files changed, 149 insertions(+), 8 deletions(-) create mode 100644 .agent/rules/leetcode.md create mode 100644 test.js diff --git a/.agent/rules/leetcode.md b/.agent/rules/leetcode.md new file mode 100644 index 0000000..e66c579 --- /dev/null +++ b/.agent/rules/leetcode.md @@ -0,0 +1,23 @@ +--- +trigger: always_on +--- + +角色设定: 你现在是一位世界顶尖的算法教练。你的目标不是直接给我答案,而是通过**“分、拆、记、跑、考”**五步法,让我从底层逻辑上完全掌握一道题目。 + +输入内容: [粘贴 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/test.js b/test.js new file mode 100644 index 0000000..9ad1ae7 --- /dev/null +++ b/test.js @@ -0,0 +1,21 @@ +var reverseKGroup = (head, k) => { + let cur = head; + let count = 0; + while (count !== k && cur !== null) { + 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 = pre; + pre = node; + node = next; + } + head.next = reverseKGroup(cur, k); + return pre; + } + return head; +} \ 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 0b7bcb6..4c8f318 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" @@ -76,13 +76,13 @@ var reverseList = function(head) { #### 3. 常见题型策略 -| 题型分类 | 关键策略 | -| :--- | :--- | -| **基础操作** | **Dummy Node** 是神器。删除、插入操作都需要找到目标节点的**前驱节点**。 | -| **反转类** | 熟练掌握 `pre`, `cur`, `next` 三指针迭代法。 | -| **合并/排序** | **归并排序**思想。找中点 -> 断开 -> 递归排序 -> 合并有序链表。 | -| **双指针技巧** | **快慢指针**找中点或判环;**双指针**分别遍历两个链表。 | -| **重排/拼接** | 组合拳:通常涉及 **找中点 + 反转后半部分 + 合并链表** 的步骤。 | +| 题型分类 | 关键策略 | +| :------------- | :---------------------------------------------------------------------- | +| **基础操作** | **Dummy Node** 是神器。删除、插入操作都需要找到目标节点的**前驱节点**。 | +| **反转类** | 熟练掌握 `pre`, `cur`, `next` 三指针迭代法。 | +| **合并/排序** | **归并排序**思想。找中点 -> 断开 -> 递归排序 -> 合并有序链表。 | +| **双指针技巧** | **快慢指针**找中点或判环;**双指针**分别遍历两个链表。 | +| **重排/拼接** | 组合拳:通常涉及 **找中点 + 反转后半部分 + 合并链表** 的步骤。 | **链表的数据结构** @@ -456,8 +456,103 @@ var copyRandomList = function(head) { }; ``` + +##### [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; +}; +``` + ## 练习 + - [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/) @@ -469,4 +564,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/) From 9ad2bbed164b6a269b0d2c546b792760d91cd28b Mon Sep 17 00:00:00 2001 From: maopb Date: Mon, 5 Jan 2026 17:58:42 +0800 Subject: [PATCH 04/17] =?UTF-8?q?feat:=20=E5=AE=8C=E5=96=84=E4=BA=8C?= =?UTF-8?q?=E5=8F=89=E6=A0=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .agent/rules/leetcode.md | 3 +- .../\344\272\214\345\217\211\346\240\221.md" | 888 +++++++++++++----- 2 files changed, 657 insertions(+), 234 deletions(-) diff --git a/.agent/rules/leetcode.md b/.agent/rules/leetcode.md index e66c579..18724c9 100644 --- a/.agent/rules/leetcode.md +++ b/.agent/rules/leetcode.md @@ -1,5 +1,6 @@ --- -trigger: always_on +trigger: model_decision +description: 讲解 --- 角色设定: 你现在是一位世界顶尖的算法教练。你的目标不是直接给我答案,而是通过**“分、拆、记、跑、考”**五步法,让我从底层逻辑上完全掌握一道题目。 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 From 97b1255bb5bef8ffdec99611bdcca64e5501ff74 Mon Sep 17 00:00:00 2001 From: maopb Date: Mon, 5 Jan 2026 18:18:23 +0800 Subject: [PATCH 05/17] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E9=AB=98?= =?UTF-8?q?=E9=A2=91=E9=97=AE=E9=A2=98=E6=80=BB=E7=BB=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- HighFrequencyQuestionsSummary.md | 142 +++++++++++++++ ...21\345\212\250\347\252\227\345\217\243.md" | 164 ++++++++++++++++++ 2 files changed, 306 insertions(+) create mode 100644 HighFrequencyQuestionsSummary.md create mode 100644 "\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" diff --git a/HighFrequencyQuestionsSummary.md b/HighFrequencyQuestionsSummary.md new file mode 100644 index 0000000..c5c8797 --- /dev/null +++ b/HighFrequencyQuestionsSummary.md @@ -0,0 +1,142 @@ +# 高频面试题分类汇总 (总计 70+ 题) + +## 一、 链表 (LinkedList) - *重中之重* + +**基础操作** +- [206. 反转链表](https://leetcode-cn.com/problems/reverse-linked-list/) (Easy) - *必背* +- [92. 反转链表 II](https://leetcode-cn.com/problems/reverse-linked-list-ii/) (Medium) - *区间反转* +- [25. K 个一组翻转链表](https://leetcode-cn.com/problems/reverse-nodes-in-k-group/) (Hard) - *面试常客* +- [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) - *归并排序* +- [补充题1. 排序奇升偶降链表](https://leetcode-cn.com/problems/sort-list/) (Medium) + +**双指针技巧** +- [141. 环形链表](https://leetcode-cn.com/problems/linked-list-cycle/) (Easy) - *判圈* +- [142. 环形链表 II](https://leetcode-cn.com/problems/linked-list-cycle-ii/) (Medium) - *找入口* +- [160. 相交链表](https://leetcode-cn.com/problems/intersection-of-two-linked-lists/) (Easy) +- [19. 删除链表的倒数第N个节点](https://leetcode-cn.com/problems/remove-nth-node-from-end-of-list/) (Medium) +- [剑指 Offer 22. 链表中倒数第k个节点](https://leetcode-cn.com/problems/lian-biao-zhong-dao-shu-di-kge-jie-dian-lcof/) (Easy) + +**综合/技巧** +- [143. 重排链表](https://leetcode-cn.com/problems/reorder-list/) (Medium) - *中点+反转+合并* +- [2. 两数相加](https://leetcode-cn.com/problems/add-two-numbers/) (Medium) +- [146. LRU缓存机制](https://leetcode-cn.com/problems/lru-cache/) (Medium) - *双向链表+哈希* +- [82. 删除排序链表中的重复元素 II](https://leetcode-cn.com/problems/remove-duplicates-from-sorted-list-ii/) (Medium) + +--- + +## 二、 二叉树 (Binary Tree) - *必考专题* + +**遍历 (BFS/DFS)** +- [102. 二叉树的层序遍历](https://leetcode-cn.com/problems/binary-tree-level-order-traversal/) (Medium) - *BFS模板* +- [103. 二叉树的锯齿形层次遍历](https://leetcode-cn.com/problems/binary-tree-zigzag-level-order-traversal/) (Medium) +- [199. 二叉树的右视图](https://leetcode-cn.com/problems/binary-tree-right-side-view/) (Medium) +- [662. 二叉树最大宽度](https://leetcode-cn.com/problems/maximum-width-of-binary-tree/) (Medium) +- [94. 二叉树的中序遍历](https://leetcode-cn.com/problems/binary-tree-inorder-traversal/) (Easy) + +**路径与属性 (分治思维)** +- [101. 对称二叉树](https://leetcode-cn.com/problems/symmetric-tree/) (Easy) +- [105. 从前序与中序遍历序列构造二叉树](https://leetcode-cn.com/problems/construct-binary-tree-from-preorder-and-inorder-traversal/) (Medium) +- [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) +- [113. 路径总和 II](https://leetcode-cn.com/problems/path-sum-ii/) (Medium) +- [129. 求根到叶子节点数字之和](https://leetcode-cn.com/problems/sum-root-to-leaf-numbers/) (Medium) +- [剑指 Offer 26. 树的子结构](https://leetcode-cn.com/problems/shu-de-zi-jie-gou-lcof/) (Medium) + +**二叉搜索树 (BST)** +- [98. 验证二叉搜索树](https://leetcode-cn.com/problems/validate-binary-search-tree/) (Medium) + +--- + +## 三、 数组与双指针 (Array & Two Pointers) + +**N数之和** +- [1. 两数之和](https://leetcode-cn.com/problems/two-sum/) (Easy) - *Hash* +- [15. 三数之和](https://leetcode-cn.com/problems/3sum/) (Medium) - *排序+双指针* + +**滑动窗口** +- [3. 无重复字符的最长子串](https://leetcode-cn.com/problems/longest-substring-without-repeating-characters/) (Medium) - *模版题* +- [209. 长度最小的子数组](https://leetcode-cn.com/problems/minimum-size-subarray-sum/) (Medium) +- [76. 最小覆盖子串](https://leetcode-cn.com/problems/minimum-window-substring/) (Hard) +- [239. 滑动窗口最大值](https://leetcode-cn.com/problems/sliding-window-maximum/) (Hard) - *单调队列* + +**经典双指针/模拟** +- [42. 接雨水](https://leetcode-cn.com/problems/trapping-rain-water/) (Hard) - *必考* +- [121. 买卖股票的最佳时机](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock/) (Easy) +- [122. 买卖股票的最佳时机 II](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-ii/) (Easy) +- [88. 合并两个有序数组](https://leetcode-cn.com/problems/merge-sorted-array/) (Easy) - *逆向指针* +- [31. 下一个排列](https://leetcode-cn.com/problems/next-permutation/) (Medium) +- [54. 螺旋矩阵](https://leetcode-cn.com/problems/spiral-matrix/) (Medium) +- [48. 旋转图像](https://leetcode-cn.com/problems/rotate-image/) (Medium) +- [56. 合并区间](https://leetcode-cn.com/problems/merge-intervals/) (Medium) +- [41. 缺失的第一个正数](https://leetcode-cn.com/problems/first-missing-positive/) (Hard) - *原地Hash* + +--- + +## 四、 动态规划 (DP) + +**基础 DP** +- [70. 爬楼梯](https://leetcode-cn.com/problems/climbing-stairs/) (Easy) +- [53. 最大子数组和](https://leetcode-cn.com/problems/maximum-subarray/) (Medium) +- [300. 最长上升子序列](https://leetcode-cn.com/problems/longest-increasing-subsequence/) (Medium) +- [322. 零钱兑换](https://leetcode-cn.com/problems/coin-change/) (Medium) - *完全背包* +- [139. 单词拆分](https://leetcode-cn.com/problems/word-break/) (Medium) + +**二维/字符串 DP** +- [1143. 最长公共子序列](https://leetcode-cn.com/problems/longest-common-subsequence/) (Medium) +- [72. 编辑距离](https://leetcode-cn.com/problems/edit-distance/) (Hard) +- [5. 最长回文子串](https://leetcode-cn.com/problems/longest-palindromic-substring/) (Medium) +- [64. 最小路径和](https://leetcode-cn.com/problems/minimum-path-sum/) (Medium) +- [221. 最大正方形](https://leetcode-cn.com/problems/maximal-square/) (Medium) + +--- + +## 五、 回溯算法 (Backtracking) + +- [46. 全排列](https://leetcode-cn.com/problems/permutations/) (Medium) - *基础* +- [78. 子集](https://leetcode-cn.com/problems/subsets/) (Medium) +- [39. 组合总和](https://leetcode-cn.com/problems/combination-sum/) (Medium) +- [93. 复原IP地址](https://leetcode-cn.com/problems/restore-ip-addresses/) (Medium) +- [22. 括号生成](https://leetcode-cn.com/problems/generate-parentheses/) (Medium) +- [79. 单词搜索](https://leetcode-cn.com/problems/word-search/) (Medium) - *网格回溯* + +--- + +## 六、 搜索 (DFS/BFS) + +- [200. 岛屿数量](https://leetcode-cn.com/problems/number-of-islands/) (Medium) - *必考* +- [695. 岛屿的最大面积](https://leetcode-cn.com/problems/max-area-of-island/) (Medium) +- [240. 搜索二维矩阵 II](https://leetcode-cn.com/problems/search-a-2d-matrix-ii/) (Medium) + +--- + +## 七、 二分查找 (Binary Search) + +- [33. 搜索旋转排序数组](https://leetcode-cn.com/problems/search-in-rotated-sorted-array/) (Medium) +- [69. x 的平方根](https://leetcode-cn.com/problems/sqrtx/) (Easy) +- [162. 寻找峰值](https://leetcode-cn.com/problems/find-peak-element/) (Medium) +- [4. 寻找两个正序数组的中位数](https://leetcode-cn.com/problems/median-of-two-sorted-arrays/) (Hard) + +--- + +## 八、 栈/字符串/数学/其他 + +**栈** +- [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) +- [232. 用栈实现队列](https://leetcode-cn.com/problems/implement-queue-using-stacks/) (Easy) +- [394. 字符串解码](https://leetcode-cn.com/problems/decode-string/) (Medium) + +**排序** +- [215. 数组中的第K个最大元素](https://leetcode-cn.com/problems/kth-largest-element-in-an-array/) (Medium) - *快速选择* +- [补充题4. 手撕快速排序](https://leetcode-cn.com/problems/kth-largest-element-in-an-array/) + +**字符串/数学** +- [415. 字符串相加](https://leetcode-cn.com/problems/add-strings/) (Easy) +- [43. 字符串相乘](https://leetcode-cn.com/problems/multiply-strings/) (Medium) +- [165. 比较版本号](https://leetcode-cn.com/problems/compare-version-numbers/) (Medium) +- [470. 用 Rand7() 实现 Rand10()](https://leetcode-cn.com/problems/implement-rand10-using-rand7/) (Medium) +- [440. 字典序的第K小数字](https://leetcode-cn.com/problems/k-th-smallest-in-lexicographical-order/) (Hard) 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..bfc8fd4 --- /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,164 @@ +# 双指针与滑动窗口 (Array & Two Pointers) + +双指针技巧是解决数组和链表问题的最强工具之一。核心思想是将两层嵌套循环 ($O(N^2)$) 优化为单层循环 ($O(N)$)。 + +主要可分为两类: +1. **快慢指针/左右指针**:解决原地修改、N数之和、接雨水等。 +2. **滑动窗口**:解决子串、子数组的匹配、长度、统计等问题。 + +--- + +## 一、 双指针通用模板 + +### 1. 左右指针 (对撞指针) +**适用场景**:二分查找、反转数组、N数之和、接雨水。 +**核心思维**:数组有序(或需要排序),双向奔赴。 + +```javascript +var twoSum = function(nums, target) { + // 1. 排序 (双指针的前提通常是有序) + nums.sort((a, b) => a - b); + + let left = 0; + let 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 变小 + } + } + return []; +}; +``` + +### 2. 快慢指针 (原地操作) +**适用场景**:删除重复元素、移动零、原地移除元素。 +**核心思维**:`slow` 维护合格区域,`fast` 探索新区域。 + +```javascript +// 示例:移除数组中的 val 元素 +var removeElement = function(nums, val) { + let slow = 0; + for (let fast = 0; fast < nums.length; fast++) { + if (nums[fast] !== val) { + nums[slow] = nums[fast]; + slow++; + } + } + return slow; +}; +``` + +--- + +## 二、 滑动窗口万能模板 (Sliding Window) + +**适用场景**: +- 寻找**最长/最短**子串或子数组 +- 统计符合条件的子串个数 +- **关键词**:连续子数组、连续子串 + +**口诀**: +1. **右移** (`right++`):扩大窗口,寻找可行解。 +2. **收缩** (`left++`):当窗口满足条件(或不满足限制)时,缩小窗口,寻找最优解。 + +```javascript +/* 滑动窗口通用模板 */ +var slidingWindow = function(s) { + // 1. 定义窗口所需的“数据结构” (Hash, Array, Counter) + const window = new Map(); + + // 2. 初始化左右边界 + let left = 0, right = 0; + + // 3. 记录结果变量 (最大长度、最小覆盖等) + let res = 0; + + // 4. 开始滑动:外层循环移动 right + while (right < s.length) { + // c 是将要移入窗口的字符 + let c = s[right]; + right++; + + // ---【进窗更新】--- + // 在这里更新窗口数据,比如 window.set(c, count + 1) + + /* debug 输出位置 */ + // console.log("window: [" + left + ", " + right + ")"); + + // 5. 判断窗口是否要收缩:内层循环移动 left + // CASE A: 求最小窗口 (如最小覆盖子串 76) -> 当窗口满足条件时,收缩以求最小 + // CASE B: 求最大窗口 (如无重复子串 3) -> 当窗口不满足条件(有重复)时,收缩直到满足 + while (/* window needs shrink */) { + // d 是将要移出窗口的字符 + let d = s[left]; + left++; + + // ---【出窗更新】--- + // 在这里更新窗口数据,比如 window.set(d, count - 1) + } + + // ---【结果更新】--- + // 如果求最大长度,通常在这里更新 res = Math.max(res, right - left) + // 如果求最小长度,通常在里面 while 循环里更新 + } + return res; +}; +``` + +--- + +## 三、 经典题型解法 + +### 1. N数之和 (Two Sum / Three Sum) +**核心**:先**排序**,固定一个数,然后转化为 Two Sum 问题。 +**注意**:去重逻辑是难点。 + +```javascript +// 三数之和伪代码 +// 1. sort +// 2. for i in 0..n: +// if nums[i] > 0 break (剪枝) +// if i > 0 && nums[i] == nums[i-1] continue (去重 i) +// left = i+1, right = n-1 +// while left < right: +// sum = nums[i] + nums[left] + nums[right] +// if sum == 0: +// res.push(...) +// while left < right && nums[left] == nums[left+1] left++ (去重 left) +// while left < right && nums[right] == nums[right-1] right-- (去重 right) +// left++, right-- +``` + +### 2. 接雨水 (Trapping Rain Water) +**核心**:`min(l_max, r_max) - height[i]` +**双指针解法**: +1. 维护 `left`, `right` 两个指针。 +2. 维护 `l_max`, `r_max` 代表左右两边的历史最高墙。 +3. 每次移动**较矮**的那一边 (木桶效应)。 + +```javascript +// 接雨水双指针极简版 +let left = 0, right = height.length - 1; +let l_max = 0, r_max = 0; +let ans = 0; +while (left < right) { + l_max = Math.max(l_max, height[left]); + r_max = Math.max(r_max, height[right]); + + // 哪边低,哪边就一定由这一边的 max 决定存水量 + if (l_max < r_max) { + ans += l_max - height[left]; + left++; + } else { + ans += r_max - height[right]; + right++; + } +} +``` From 318d1d36465a277428b6763ebbfda7bf1a635cf9 Mon Sep 17 00:00:00 2001 From: mpbfx <1835886801@qq.com> Date: Mon, 5 Jan 2026 23:56:18 +0800 Subject: [PATCH 06/17] =?UTF-8?q?feat:=20=E5=AE=8C=E5=96=84=E5=8F=8C?= =?UTF-8?q?=E6=8C=87=E9=92=88=E4=B8=8E=E6=BB=91=E5=8A=A8=E7=AA=97=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...21\345\212\250\347\252\227\345\217\243.md" | 384 +++++++++++++----- 1 file changed, 273 insertions(+), 111 deletions(-) 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" index bfc8fd4..9fa818b 100644 --- "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" @@ -1,31 +1,27 @@ -# 双指针与滑动窗口 (Array & Two Pointers) +# 双指针与滑动窗口 (Two Pointers & Sliding Window) -双指针技巧是解决数组和链表问题的最强工具之一。核心思想是将两层嵌套循环 ($O(N^2)$) 优化为单层循环 ($O(N)$)。 - -主要可分为两类: -1. **快慢指针/左右指针**:解决原地修改、N数之和、接雨水等。 -2. **滑动窗口**:解决子串、子数组的匹配、长度、统计等问题。 +## 核心哲学 +双指针技巧的本质是**利用数组的单调性(有序性)或问题的连续性**,通过两个指针的移动,将原本需要两层嵌套循环 ($O(N^2)$) 的搜索空间,优化为单次遍历 ($O(N)$)。 --- -## 一、 双指针通用模板 +## 一、 通解与模板:三大思维模式 -### 1. 左右指针 (对撞指针) -**适用场景**:二分查找、反转数组、N数之和、接雨水。 -**核心思维**:数组有序(或需要排序),双向奔赴。 +### 1. 左右指针 (对撞指针 / Opposite Pointers) +**核心思想**:指针从两端向中间汇合。 +**适用场景**:有序数组搜索、反转、回文判断、接雨水。 -```javascript -var twoSum = function(nums, target) { - // 1. 排序 (双指针的前提通常是有序) - nums.sort((a, b) => a - b); - - let left = 0; - let right = nums.length - 1; - +**口诀**: +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 变大 @@ -33,80 +29,77 @@ var twoSum = function(nums, target) { right--; // 让 sum 变小 } } - return []; }; ``` -### 2. 快慢指针 (原地操作) -**适用场景**:删除重复元素、移动零、原地移除元素。 -**核心思维**:`slow` 维护合格区域,`fast` 探索新区域。 +### 2. 快慢指针 (同向指针 / Fast-Slow Pointers) +**核心思想**:两个指针同向移动,速度不同或起点不同。 +**适用场景**: +- **数组**:原地修改(去重、移动元素)。 +- **链表**:找中点、判断环、找倒数第 K 个。 -```javascript -// 示例:移除数组中的 val 元素 -var removeElement = function(nums, val) { - let slow = 0; - for (let fast = 0; fast < nums.length; fast++) { - if (nums[fast] !== val) { +```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; + return slow; // 返回新长度 }; ``` ---- - -## 二、 滑动窗口万能模板 (Sliding Window) +### 3. 滑动窗口 (Sliding Window - 最强模板) +**核心思想**:维护一个“窗口”,通过扩大和缩小窗口来寻找满足条件的子区间。 +**适用场景**:子串匹配、连续子数组和/长度。 -**适用场景**: -- 寻找**最长/最短**子串或子数组 -- 统计符合条件的子串个数 -- **关键词**:连续子数组、连续子串 - -**口诀**: -1. **右移** (`right++`):扩大窗口,寻找可行解。 -2. **收缩** (`left++`):当窗口满足条件(或不满足限制)时,缩小窗口,寻找最优解。 - -```javascript -/* 滑动窗口通用模板 */ -var slidingWindow = function(s) { - // 1. 定义窗口所需的“数据结构” (Hash, Array, Counter) - const window = new Map(); +**万能模板 (背诵全文)**: +```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); - // 2. 初始化左右边界 let left = 0, right = 0; - - // 3. 记录结果变量 (最大长度、最小覆盖等) - let res = 0; + let valid = 0; // 窗口中满足 need 条件的字符种类数 + let res = /* 根据题目要求初始化 */; - // 4. 开始滑动:外层循环移动 right while (right < s.length) { - // c 是将要移入窗口的字符 + // a. 【入窗】:c 是将要移入窗口的字符 let c = s[right]; - right++; - - // ---【进窗更新】--- - // 在这里更新窗口数据,比如 window.set(c, count + 1) + right++; // 右移窗口 - /* debug 输出位置 */ - // console.log("window: [" + left + ", " + right + ")"); + // --- 进行窗口内数据的一系列更新 --- + if (need.has(c)) { + window.set(c, (window.get(c) || 0) + 1); + if (window.get(c) === need.get(c)) valid++; + } - // 5. 判断窗口是否要收缩:内层循环移动 left - // CASE A: 求最小窗口 (如最小覆盖子串 76) -> 当窗口满足条件时,收缩以求最小 - // CASE B: 求最大窗口 (如无重复子串 3) -> 当窗口不满足条件(有重复)时,收缩直到满足 + // b. 【收缩】:判断左侧窗口是否要收缩 + // 对于求“最小”窗口:当满足条件时收缩,以寻找更短的可能 + // 对于求“最大”窗口:当不满足限制时收缩,以恢复合法状态 while (/* window needs shrink */) { // d 是将要移出窗口的字符 let d = s[left]; - left++; + left++; // 左移窗口 - // ---【出窗更新】--- - // 在这里更新窗口数据,比如 window.set(d, count - 1) + // --- 进行窗口内数据的一系列更新 --- + if (need.has(d)) { + if (window.get(d) === need.get(d)) valid--; + window.set(d, window.get(d) - 1); + } } - // ---【结果更新】--- - // 如果求最大长度,通常在这里更新 res = Math.max(res, right - left) - // 如果求最小长度,通常在里面 while 循环里更新 + // c. 【更新结果】:根据题目要求在合适的位置更新 res + // 如果求最大长度,通常在 while 外更新:res = Math.max(res, right - left) + // 如果求最小长度,通常在 while 内更新:res = Math.min(res, right - left) } return res; }; @@ -114,51 +107,220 @@ var slidingWindow = function(s) { --- -## 三、 经典题型解法 - -### 1. N数之和 (Two Sum / Three Sum) -**核心**:先**排序**,固定一个数,然后转化为 Two Sum 问题。 -**注意**:去重逻辑是难点。 - -```javascript -// 三数之和伪代码 -// 1. sort -// 2. for i in 0..n: -// if nums[i] > 0 break (剪枝) -// if i > 0 && nums[i] == nums[i-1] continue (去重 i) -// left = i+1, right = n-1 -// while left < right: -// sum = nums[i] + nums[left] + nums[right] -// if sum == 0: -// res.push(...) -// while left < right && nums[left] == nums[left+1] left++ (去重 left) -// while left < right && nums[right] == nums[right-1] right-- (去重 right) -// left++, right-- +## 二、 记忆心法总结 + +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. 接雨水 (Trapping Rain Water) -**核心**:`min(l_max, r_max) - height[i]` -**双指针解法**: -1. 维护 `left`, `right` 两个指针。 -2. 维护 `l_max`, `r_max` 代表左右两边的历史最高墙。 -3. 每次移动**较矮**的那一边 (木桶效应)。 - -```javascript -// 接雨水双指针极简版 -let left = 0, right = height.length - 1; -let l_max = 0, r_max = 0; -let ans = 0; -while (left < right) { - l_max = Math.max(l_max, height[left]); - r_max = Math.max(r_max, height[right]); - - // 哪边低,哪边就一定由这一边的 max 决定存水量 - if (l_max < r_max) { - ans += l_max - height[left]; - left++; - } else { - ans += r_max - height[right]; +### 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. **判断环 (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; +}; ``` From 2b30259e35c9c6efba50ff790e1426433107ac12 Mon Sep 17 00:00:00 2001 From: mpbfx <1835886801@qq.com> Date: Tue, 6 Jan 2026 00:04:14 +0800 Subject: [PATCH 07/17] =?UTF-8?q?feat:=20=E5=AE=8C=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- HighFrequencyQuestionsSummary.md | 18 +- ...21\345\212\250\347\252\227\345\217\243.md" | 193 ++++++++++++++++++ 2 files changed, 205 insertions(+), 6 deletions(-) diff --git a/HighFrequencyQuestionsSummary.md b/HighFrequencyQuestionsSummary.md index c5c8797..457a901 100644 --- a/HighFrequencyQuestionsSummary.md +++ b/HighFrequencyQuestionsSummary.md @@ -52,24 +52,30 @@ ## 三、 数组与双指针 (Array & Two Pointers) -**N数之和** -- [1. 两数之和](https://leetcode-cn.com/problems/two-sum/) (Easy) - *Hash* +**基础双指针 (Two Pointers Basics)** +- [167. 两数之和 II](https://leetcode-cn.com/problems/two-sum-ii-input-array-is-sorted/) (Easy) - *左右指针* - [15. 三数之和](https://leetcode-cn.com/problems/3sum/) (Medium) - *排序+双指针* +- [42. 接雨水](https://leetcode-cn.com/problems/trapping-rain-water/) (Hard) - *必考* -**滑动窗口** +**滑动窗口 (Sliding Window)** - [3. 无重复字符的最长子串](https://leetcode-cn.com/problems/longest-substring-without-repeating-characters/) (Medium) - *模版题* - [209. 长度最小的子数组](https://leetcode-cn.com/problems/minimum-size-subarray-sum/) (Medium) - [76. 最小覆盖子串](https://leetcode-cn.com/problems/minimum-window-substring/) (Hard) - [239. 滑动窗口最大值](https://leetcode-cn.com/problems/sliding-window-maximum/) (Hard) - *单调队列* -**经典双指针/模拟** -- [42. 接雨水](https://leetcode-cn.com/problems/trapping-rain-water/) (Hard) - *必考* +**贪心策略 (Greedy Strategy)** - [121. 买卖股票的最佳时机](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock/) (Easy) - [122. 买卖股票的最佳时机 II](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-ii/) (Easy) + +**逆向与排列双指针 (Reverse & Permutation)** - [88. 合并两个有序数组](https://leetcode-cn.com/problems/merge-sorted-array/) (Easy) - *逆向指针* -- [31. 下一个排列](https://leetcode-cn.com/problems/next-permutation/) (Medium) +- [31. 下一个排列](https://leetcode-cn.com/problems/next-permutation/) (Medium) - *找逆序对+反转* + +**二维矩阵模拟 (Matrix Simulation)** - [54. 螺旋矩阵](https://leetcode-cn.com/problems/spiral-matrix/) (Medium) - [48. 旋转图像](https://leetcode-cn.com/problems/rotate-image/) (Medium) + +**区间与原地哈希 (Intervals & Cyclic Sort)** - [56. 合并区间](https://leetcode-cn.com/problems/merge-intervals/) (Medium) - [41. 缺失的第一个正数](https://leetcode-cn.com/problems/first-missing-positive/) (Hard) - *原地Hash* 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" index 9fa818b..17e4a88 100644 --- "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" @@ -304,8 +304,201 @@ var trap = function(height) { --- +## 五、 广义双指针与数组技巧拓展 + +### 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 步,然后同步走。 From 57ff5932895fdd8703ea63b7ce680e9e0be0b1a6 Mon Sep 17 00:00:00 2001 From: maopb Date: Thu, 8 Jan 2026 18:20:30 +0800 Subject: [PATCH 08/17] feat: 1/8 --- HighFrequencyQuestionsSummary.md | 259 +++++++++++++++++++++++++++---- 1 file changed, 228 insertions(+), 31 deletions(-) diff --git a/HighFrequencyQuestionsSummary.md b/HighFrequencyQuestionsSummary.md index c5c8797..f58bae5 100644 --- a/HighFrequencyQuestionsSummary.md +++ b/HighFrequencyQuestionsSummary.md @@ -62,53 +62,250 @@ - [76. 最小覆盖子串](https://leetcode-cn.com/problems/minimum-window-substring/) (Hard) - [239. 滑动窗口最大值](https://leetcode-cn.com/problems/sliding-window-maximum/) (Hard) - *单调队列* -**经典双指针/模拟** -- [42. 接雨水](https://leetcode-cn.com/problems/trapping-rain-water/) (Hard) - *必考* -- [121. 买卖股票的最佳时机](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock/) (Easy) -- [122. 买卖股票的最佳时机 II](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-ii/) (Easy) -- [88. 合并两个有序数组](https://leetcode-cn.com/problems/merge-sorted-array/) (Easy) - *逆向指针* -- [31. 下一个排列](https://leetcode-cn.com/problems/next-permutation/) (Medium) -- [54. 螺旋矩阵](https://leetcode-cn.com/problems/spiral-matrix/) (Medium) -- [48. 旋转图像](https://leetcode-cn.com/problems/rotate-image/) (Medium) -- [56. 合并区间](https://leetcode-cn.com/problems/merge-intervals/) (Medium) -- [41. 缺失的第一个正数](https://leetcode-cn.com/problems/first-missing-positive/) (Hard) - *原地Hash* +**对撞双指针** *(左右向中间逼近)* +- [42. 接雨水](https://leetcode-cn.com/problems/trapping-rain-water/) (Hard) - *必考,左右指针+维护最大高度* +- [88. 合并两个有序数组](https://leetcode-cn.com/problems/merge-sorted-array/) (Easy) - *逆向双指针,从后往前填充* +- [11. 盛最多水的容器](https://leetcode-cn.com/problems/container-with-most-water/) (Medium) - *移动较短的一边* + +**快慢指针** *(同向不同速)* +- [26. 删除有序数组中的重复项](https://leetcode-cn.com/problems/remove-duplicates-from-sorted-array/) (Easy) - *slow记录有效位置* +- [27. 移除元素](https://leetcode-cn.com/problems/remove-element/) (Easy) - *原地删除* +- [283. 移动零](https://leetcode-cn.com/problems/move-zeroes/) (Easy) - *非零前移,尾部填零* +- [287. 寻找重复数](https://leetcode-cn.com/problems/find-the-duplicate-number/) (Medium) - *Floyd判圈法* + +**贪心策略** +- [121. 买卖股票的最佳时机](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock/) (Easy) - *维护历史最低价* +- [122. 买卖股票的最佳时机 II](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-ii/) (Medium) - *累加所有正收益* + +**矩阵模拟 (边界控制)** +- [54. 螺旋矩阵](https://leetcode-cn.com/problems/spiral-matrix/) (Medium) - *四边界收缩* +- [48. 旋转图像](https://leetcode-cn.com/problems/rotate-image/) (Medium) - *先转置后翻转 / 四角轮换* +- [31. 下一个排列](https://leetcode-cn.com/problems/next-permutation/) (Medium) - *规律模拟:找降序起点* + +**区间处理 & 原地哈希** +- [56. 合并区间](https://leetcode-cn.com/problems/merge-intervals/) (Medium) - *排序后贪心合并* +- [41. 缺失的第一个正数](https://leetcode-cn.com/problems/first-missing-positive/) (Hard) - *原地哈希:nums[i]放到i-1位置* --- ## 四、 动态规划 (DP) -**基础 DP** -- [70. 爬楼梯](https://leetcode-cn.com/problems/climbing-stairs/) (Easy) -- [53. 最大子数组和](https://leetcode-cn.com/problems/maximum-subarray/) (Medium) -- [300. 最长上升子序列](https://leetcode-cn.com/problems/longest-increasing-subsequence/) (Medium) -- [322. 零钱兑换](https://leetcode-cn.com/problems/coin-change/) (Medium) - *完全背包* -- [139. 单词拆分](https://leetcode-cn.com/problems/word-break/) (Medium) +> **DP四要素**:① 状态定义 → ② 转移方程 → ③ 初始化 → ④ 返回值 -**二维/字符串 DP** -- [1143. 最长公共子序列](https://leetcode-cn.com/problems/longest-common-subsequence/) (Medium) -- [72. 编辑距离](https://leetcode-cn.com/problems/edit-distance/) (Hard) -- [5. 最长回文子串](https://leetcode-cn.com/problems/longest-palindromic-substring/) (Medium) -- [64. 最小路径和](https://leetcode-cn.com/problems/minimum-path-sum/) (Medium) -- [221. 最大正方形](https://leetcode-cn.com/problems/maximal-square/) (Medium) +--- + +### 模板一:线性DP (单序列) + +> **状态**:`dp[i]` = 以 `nums[i]` 结尾/到达位置 `i` 的最优解 +> **特点**:当前状态只依赖前面的状态 + +```javascript +// 通用模板 +const dp = new Array(n).fill(初始值); +dp[0] = 边界值; +for (let i = 1; i < n; i++) { + dp[i] = 状态转移(dp[i-1], dp[i-2], ...); // 或遍历 j < i +} +return dp[n-1]; // 或 Math.max(...dp) +``` + +| 题目 | 状态定义 | 转移方程 | +| --------------------------------------------------------------------------------------- | --------------------------- | ---------------------------------------------------------- | +| [70. 爬楼梯](https://leetcode-cn.com/problems/climbing-stairs/) | `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/) | `dp[i]` = 前i个字符能否拆分 | `dp[i] = dp[j] && s[j:i] in dict` | + +--- + +### 模板二:完全背包DP + +> **状态**:`dp[i]` = 凑成金额/容量 `i` 的最优解 +> **特点**:物品可重复选取,外层遍历目标,内层遍历选择 + +```javascript +// 完全背包模板 +const dp = new Array(amount + 1).fill(Infinity); +dp[0] = 0; +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); + } + } +} +return dp[amount] > amount ? -1 : dp[amount]; +``` + +| 题目 | 状态定义 | 转移方程 | +| -------------------------------------------------------------- | --------------------------- | ------------------------------- | +| [322. 零钱兑换](https://leetcode-cn.com/problems/coin-change/) | `dp[i]` = 凑成i的最少硬币数 | `dp[i] = min(dp[i - coin] + 1)` | + +--- + +### 模板三:双序列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)); +// 初始化边界 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; // 匹配 + } else { + dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]); // 不匹配 + } + } +} +return dp[m][n]; +``` + +| 题目 | 状态定义 | 转移方程 | +| ------------------------------------------------------------------------------------ | ----------------------- | ----------------------------------------------------------- | +| [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/) | `dp[i][j]` = 最少操作数 | 相等:`dp[i-1][j-1]`;不等:`min(三方向) + 1` | + +--- + +### 模板四:矩阵/区间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++) { // 枚举区间长度 + for (let i = 0; i + len - 1 < n; i++) { // 枚举左端点 + let j = i + len - 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/) | `dp[i][j]` = 以(i,j)为右下角的最大边长 | `dp[i][j] = min(左,上,左上) + 1` | +| [5. 最长回文子串](https://leetcode-cn.com/problems/longest-palindromic-substring/) | `dp[i][j]` = s[i..j]是否回文 | `dp[i][j] = s[i]==s[j] && dp[i+1][j-1]` | --- ## 五、 回溯算法 (Backtracking) -- [46. 全排列](https://leetcode-cn.com/problems/permutations/) (Medium) - *基础* -- [78. 子集](https://leetcode-cn.com/problems/subsets/) (Medium) -- [39. 组合总和](https://leetcode-cn.com/problems/combination-sum/) (Medium) -- [93. 复原IP地址](https://leetcode-cn.com/problems/restore-ip-addresses/) (Medium) -- [22. 括号生成](https://leetcode-cn.com/problems/generate-parentheses/) (Medium) -- [79. 单词搜索](https://leetcode-cn.com/problems/word-search/) (Medium) - *网格回溯* +> **核心思想**:选择 → 递归 → 撤销(回溯) +> **时间复杂度**:通常 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/) | 组合 | 可重复选,下一层从 `i` 开始 | +| [93. 复原IP地址](https://leetcode-cn.com/problems/restore-ip-addresses/) | 分割 | 每段 1-3 位,值 ≤ 255,无前导零 | +| [22. 括号生成](https://leetcode-cn.com/problems/generate-parentheses/) | 决策树 | `left < n` 可加左括号,`right < left` 可加右括号 | +| [79. 单词搜索](https://leetcode-cn.com/problems/word-search/) | 网格回溯 | 四方向 DFS + 原地标记访问 | --- ## 六、 搜索 (DFS/BFS) -- [200. 岛屿数量](https://leetcode-cn.com/problems/number-of-islands/) (Medium) - *必考* -- [695. 岛屿的最大面积](https://leetcode-cn.com/problems/max-area-of-island/) (Medium) -- [240. 搜索二维矩阵 II](https://leetcode-cn.com/problems/search-a-2d-matrix-ii/) (Medium) +### 网格DFS模板 (岛屿问题) + +```javascript +function dfs(grid, i, j) { + // 边界检查 + 访问检查 + if (i < 0 || i >= m || j < 0 || j >= n) return 0; + if (grid[i][j] !== '1') return 0; + + grid[i][j] = '0'; // 标记已访问(原地修改 / 或用visited数组) + + // 四方向递归 + 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]]; + const dirs = [[1,0], [-1,0], [0,1], [0,-1]]; + + while (queue.length) { + const [i, j] = queue.shift(); + for (const [di, dj] of dirs) { + const ni = i + di, nj = j + dj; + if (ni >= 0 && ni < m && nj >= 0 && nj < n && grid[ni][nj] === '1') { + grid[ni][nj] = '0'; + queue.push([ni, nj]); + } + } + } +} +``` + +### 题目速查 + +| 题目 | 算法 | 关键技巧 | +| ------------------------------------------------------------------------------- | ------- | -------------------------------- | +| [200. 岛屿数量](https://leetcode-cn.com/problems/number-of-islands/) | 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/) | 双指针 | 从右上角开始,大了往左,小了往下 | --- From 2b7162fba993a0b7f6a73357f2198702a8f4e602 Mon Sep 17 00:00:00 2001 From: mpbfx <1835886801@qq.com> Date: Fri, 9 Jan 2026 01:14:11 +0800 Subject: [PATCH 09/17] feat: 1/9 --- HighFrequencyQuestionsSummary.md | 390 ++++++++++++++++++++++++++++--- 1 file changed, 358 insertions(+), 32 deletions(-) diff --git a/HighFrequencyQuestionsSummary.md b/HighFrequencyQuestionsSummary.md index 90bf382..2c9c06a 100644 --- a/HighFrequencyQuestionsSummary.md +++ b/HighFrequencyQuestionsSummary.md @@ -102,12 +102,12 @@ ```javascript // 通用模板 -const dp = new Array(n).fill(初始值); -dp[0] = 边界值; +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], ...); // 或遍历 j < i + dp[i] = 状态转移(dp[i-1], dp[i-2], ...); // 3. 转移方程 } -return dp[n-1]; // 或 Math.max(...dp) +return dp[n-1]; // 4. 返回值 (或 Math.max(...dp)) ``` | 题目 | 状态定义 | 转移方程 | @@ -117,6 +117,68 @@ return dp[n-1]; // 或 Math.max(...dp) | [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/) | `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 @@ -126,22 +188,44 @@ return dp[n-1]; // 或 Math.max(...dp) ```javascript // 完全背包模板 -const dp = new Array(amount + 1).fill(Infinity); -dp[0] = 0; -for (let i = 1; i <= amount; i++) { - for (const coin of coins) { +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); + dp[i] = Math.min(dp[i], dp[i - coin] + 1); // 3. 转移方程 } } } -return dp[amount] > amount ? -1 : dp[amount]; +return dp[amount] > amount ? -1 : dp[amount]; // 4. 返回值 ``` | 题目 | 状态定义 | 转移方程 | | -------------------------------------------------------------- | --------------------------- | ------------------------------- | | [322. 零钱兑换](https://leetcode-cn.com/problems/coin-change/) | `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 @@ -151,18 +235,18 @@ return dp[amount] > amount ? -1 : dp[amount]; ```javascript // 双序列DP模板 -const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0)); -// 初始化边界 dp[0][j] 和 dp[i][0] +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; // 匹配 + dp[i][j] = dp[i-1][j-1] + 1; // 3. 转移方程:匹配 } else { - dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]); // 不匹配 + dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]); // 3. 转移方程:不匹配 } } } -return dp[m][n]; +return dp[m][n]; // 4. 答案 ``` | 题目 | 状态定义 | 转移方程 | @@ -170,6 +254,48 @@ return dp[m][n]; | [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/) | `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 @@ -181,14 +307,16 @@ return dp[m][n]; // 矩阵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++) { // 枚举区间长度 - for (let i = 0; i + len - 1 < n; i++) { // 枚举左端点 - let j = i + len - 1; // 右端点 +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] 等子区间计算; } } @@ -200,6 +328,74 @@ for (let len = 2; len <= n; len++) { // 枚举区间长度 | [221. 最大正方形](https://leetcode-cn.com/problems/maximal-square/) | `dp[i][j]` = 以(i,j)为右下角的最大边长 | `dp[i][j] = min(左,上,左上) + 1` | | [5. 最长回文子串](https://leetcode-cn.com/problems/longest-palindromic-substring/) | `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) @@ -215,18 +411,18 @@ const result = []; function backtrack(path, choices, start) { // 1. 满足结束条件,收集结果 if (满足结束条件) { - result.push([...path]); // 注意拷贝 + result.push([...path]); // 注意拷贝,避免引用问题 return; } // 2. 遍历选择列表 for (let i = start; i < choices.length; i++) { - // 剪枝(可选) + // 剪枝(可选):排除不合法的选择 if (需要剪枝) continue; - path.push(choices[i]); // 做选择 - backtrack(path, choices, 下一个起点); // 递归 - path.pop(); // 撤销选择 + path.push(choices[i]); // 做选择:将选择加入路径 + backtrack(path, choices, 下一个起点); // 递归:进入下一层决策树 + path.pop(); // 撤销选择:回溯,恢复状态 } } ``` @@ -250,6 +446,134 @@ function backtrack(path, choices, start) { | [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) @@ -258,23 +582,24 @@ function backtrack(path, choices, start) { ```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; - grid[i][j] = '0'; // 标记已访问(原地修改 / 或用visited数组) + // 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); + count++; // 发现新岛屿 + dfs(grid, i, j); // 沉没整个岛屿 } } } @@ -284,15 +609,16 @@ for (let i = 0; i < m; i++) { ```javascript function bfs(grid, startI, startJ) { - const queue = [[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(); + 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'; + grid[ni][nj] = '0'; // 4. 标记访问并入队 queue.push([ni, nj]); } } From 83ffb9a52b553f02aa61e4dc63e305d2a4125a86 Mon Sep 17 00:00:00 2001 From: maopb Date: Fri, 9 Jan 2026 18:17:15 +0800 Subject: [PATCH 10/17] feat: 1/9 --- HighFrequencyQuestionsSummary.md | 420 +++++++++++++++++++++++++++++++ 1 file changed, 420 insertions(+) diff --git a/HighFrequencyQuestionsSummary.md b/HighFrequencyQuestionsSummary.md index 2c9c06a..ea8e4f8 100644 --- a/HighFrequencyQuestionsSummary.md +++ b/HighFrequencyQuestionsSummary.md @@ -643,6 +643,172 @@ function bfs(grid, startI, startJ) { - [162. 寻找峰值](https://leetcode-cn.com/problems/find-peak-element/) (Medium) - [4. 寻找两个正序数组的中位数](https://leetcode-cn.com/problems/median-of-two-sorted-arrays/) (Hard) +### 核心模板(推荐使用) + +```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. 平方根 | 边界处理 | `mid * mid` 与 x 比较 | +| 162. 峰值 | 方向选择 | `nums[mid] > nums[mid+1]` | +| 4. 中位数 | 双数组分割 | 分割点满足交叉条件 | + --- ## 八、 栈/字符串/数学/其他 @@ -654,10 +820,264 @@ function bfs(grid, startI, startJ) { - [232. 用栈实现队列](https://leetcode-cn.com/problems/implement-queue-using-stacks/) (Easy) - [394. 字符串解码](https://leetcode-cn.com/problems/decode-string/) (Medium) +### 栈的通用思路 + +栈题目没有固定模板,但有通用思路: + +| 场景 | 思路 | 栈中存储 | +|------|------|----------| +| 匹配问题 | 遇"左"入栈,遇"右"出栈匹配 | 字符 | +| 计算区间长度 | 存索引而非值 | 索引 | +| 维护额外信息 | 辅助栈同步维护 | 值 + 辅助信息 | +| 逆序处理 | 双栈倒腾 | 分离存储 | + +### 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) - *快速选择* - [补充题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) - [43. 字符串相乘](https://leetcode-cn.com/problems/multiply-strings/) (Medium) From ef1901513400c5a7be07a678112a262a9f9c5e0e Mon Sep 17 00:00:00 2001 From: maopb Date: Mon, 12 Jan 2026 18:06:59 +0800 Subject: [PATCH 11/17] feat: 1/12 --- HighFrequencyQuestionsSummary.md | 165 ++++++++++++++++++++++--------- 1 file changed, 116 insertions(+), 49 deletions(-) diff --git a/HighFrequencyQuestionsSummary.md b/HighFrequencyQuestionsSummary.md index ea8e4f8..5822a47 100644 --- a/HighFrequencyQuestionsSummary.md +++ b/HighFrequencyQuestionsSummary.md @@ -4,25 +4,41 @@ **基础操作** - [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` ,删除所有含有重复数字的节点,只保留原始链表中未重复出现的数字。 --- @@ -30,23 +46,37 @@ **遍历 (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` ,判断其是否是一个有效的二叉搜索树。 --- @@ -54,38 +84,59 @@ **基础双指针 (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` 天的价格。在每一天,你可以决定是否购买和/或出售股票。计算你所能获得的最大利润。 **矩阵模拟 (边界控制)** - [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` 的下一个字典序更大的排列。 **区间处理 & 原地哈希** - [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)$ 空间复杂度)。 --- @@ -110,12 +161,12 @@ for (let i = 1; i < n; i++) { return dp[n-1]; // 4. 返回值 (或 Math.max(...dp)) ``` -| 题目 | 状态定义 | 转移方程 | -| --------------------------------------------------------------------------------------- | --------------------------- | ---------------------------------------------------------- | -| [70. 爬楼梯](https://leetcode-cn.com/problems/climbing-stairs/) | `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/) | `dp[i]` = 前i个字符能否拆分 | `dp[i] = dp[j] && s[j:i] in dict` | +| 题目 | 题干 | 状态定义 | 转移方程 | +| :-------------------------------------------------------------------------------------- | :---------------------------------------------------- | :-------------------------- | :--------------------------------------------------------- | +| [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` |
代码实现 (点击展开) @@ -200,9 +251,9 @@ for (let i = 1; i <= amount; i++) { // 遍历容量 return dp[amount] > amount ? -1 : dp[amount]; // 4. 返回值 ``` -| 题目 | 状态定义 | 转移方程 | -| -------------------------------------------------------------- | --------------------------- | ------------------------------- | -| [322. 零钱兑换](https://leetcode-cn.com/problems/coin-change/) | `dp[i]` = 凑成i的最少硬币数 | `dp[i] = min(dp[i - coin] + 1)` | +| 题目 | 题干 | 状态定义 | 转移方程 | +| :------------------------------------------------------------- | :------------------------------------- | :-------------------------- | :------------------------------ | +| [322. 零钱兑换](https://leetcode-cn.com/problems/coin-change/) | 凑成总金额 `amount` 所需的最少硬币个数 | `dp[i]` = 凑成i的最少硬币数 | `dp[i] = min(dp[i - coin] + 1)` |
代码实现 (点击展开) @@ -249,10 +300,10 @@ for (let i = 1; i <= m; i++) { 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/) | `dp[i][j]` = 最少操作数 | 相等:`dp[i-1][j-1]`;不等:`min(三方向) + 1` | +| 题目 | 题干 | 状态定义 | 转移方程 | +| :----------------------------------------------------------------------------------- | :------------------------------------------------- | :---------------------- | :---------------------------------------------------------- | +| [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` |
代码实现 (点击展开) @@ -322,11 +373,11 @@ for (let len = 2; len <= n; len++) { // 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/) | `dp[i][j]` = 以(i,j)为右下角的最大边长 | `dp[i][j] = min(左,上,左上) + 1` | -| [5. 最长回文子串](https://leetcode-cn.com/problems/longest-palindromic-substring/) | `dp[i][j]` = s[i..j]是否回文 | `dp[i][j] = s[i]==s[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]` |
代码实现 (点击展开) @@ -437,14 +488,14 @@ function backtrack(path, choices, start) { ### 题目速查 -| 题目 | 类型 | 关键技巧 | -| ------------------------------------------------------------------------ | -------- | ------------------------------------------------ | -| [46. 全排列](https://leetcode-cn.com/problems/permutations/) | 排列 | `visited` 数组标记已用元素 | -| [78. 子集](https://leetcode-cn.com/problems/subsets/) | 子集 | 每个节点都收集结果 | -| [39. 组合总和](https://leetcode-cn.com/problems/combination-sum/) | 组合 | 可重复选,下一层从 `i` 开始 | -| [93. 复原IP地址](https://leetcode-cn.com/problems/restore-ip-addresses/) | 分割 | 每段 1-3 位,值 ≤ 255,无前导零 | -| [22. 括号生成](https://leetcode-cn.com/problems/generate-parentheses/) | 决策树 | `left < n` 可加左括号,`right < left` 可加右括号 | -| [79. 单词搜索](https://leetcode-cn.com/problems/word-search/) | 网格回溯 | 四方向 DFS + 原地标记访问 | +| 题目 | 题干 | 类型 | 关键技巧 | +| :----------------------------------------------------------------------- | :--------------------------------------------------------- | :------- | :----------------------------------------------- | +| [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 + 原地标记访问 |
代码实现 (点击展开) @@ -628,20 +679,24 @@ function bfs(grid, startI, startJ) { ### 题目速查 -| 题目 | 算法 | 关键技巧 | -| ------------------------------------------------------------------------------- | ------- | -------------------------------- | -| [200. 岛屿数量](https://leetcode-cn.com/problems/number-of-islands/) | 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/) | 双指针 | 从右上角开始,大了往左,小了往下 | +| 题目 | 题干 | 算法 | 关键技巧 | +| :------------------------------------------------------------------------------ | :--------------------------------------------------- | :------ | :------------------------------- | +| [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/) | 在行和列都升序排列的矩阵中搜索目标值 | 双指针 | 从右上角开始,大了往左,小了往下 | --- ## 七、 二分查找 (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`。请你找出这两个正序数组的中位数。 ### 核心模板(推荐使用) @@ -802,12 +857,12 @@ var findMedianSortedArrays = function(nums1, nums2) { ### 总结对比 -| 题目 | 难点 | 二分条件 | -|------|------|----------| -| 33. 旋转数组 | 判断有序半边 | `nums[left] < nums[mid]` | -| 69. 平方根 | 边界处理 | `mid * mid` 与 x 比较 | -| 162. 峰值 | 方向选择 | `nums[mid] > nums[mid+1]` | -| 4. 中位数 | 双数组分割 | 分割点满足交叉条件 | +| 题目 | 题干 | 难点 | 二分条件 | +| :----------- | :----------------------------- | :----------- | :------------------------ | +| 33. 旋转数组 | 在旋转后的升序数组中搜索目标值 | 判断有序半边 | `nums[left] < nums[mid]` | +| 69. 平方根 | 计算非负整数 x 的算术平方根 | 边界处理 | `mid * mid` 与 x 比较 | +| 162. 峰值 | 在数组中寻找任意一个峰值索引 | 方向选择 | `nums[mid] > nums[mid+1]` | +| 4. 中位数 | 找两个正序数组的中位数 | 双数组分割 | 分割点满足交叉条件 | --- @@ -815,21 +870,26 @@ var findMedianSortedArrays = function(nums1, nums2) { **栈** - [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) @@ -977,7 +1037,9 @@ var decodeString = function(s) { **排序** - [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/) + > 给定一个数组,实现快速排序算法。 ### 快速排序模板 @@ -1071,16 +1133,21 @@ function partition(nums, left, right) { ### 排序算法对比 -| 算法 | 时间复杂度 | 空间复杂度 | 稳定性 | 适用场景 | -|------|-----------|-----------|--------|----------| -| 快速排序 | O(nlogn) 平均 | O(logn) | 不稳定 | 通用排序 | -| 快速选择 | O(n) 平均 | O(1) | - | 找第K大/小 | -| 堆排序 | O(nlogn) | O(1) | 不稳定 | TopK问题 | -| 归并排序 | O(nlogn) | O(n) | 稳定 | 链表排序、求逆序对 | +| 算法 | 时间复杂度 | 空间复杂度 | 稳定性 | 适用场景 | +| -------- | ------------- | ---------- | ------ | ------------------ | +| 快速排序 | 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` 小的整数。 From 21e8fb9af8bd00fd59e5aaf020a86dc84dad9a79 Mon Sep 17 00:00:00 2001 From: maopb Date: Tue, 13 Jan 2026 14:20:57 +0800 Subject: [PATCH 12/17] feat: 1/13 --- HighFrequencyQuestionsSummary.md | 386 +++++++++++++++++++++++++++++++ 1 file changed, 386 insertions(+) diff --git a/HighFrequencyQuestionsSummary.md b/HighFrequencyQuestionsSummary.md index 5822a47..bb6af59 100644 --- a/HighFrequencyQuestionsSummary.md +++ b/HighFrequencyQuestionsSummary.md @@ -124,6 +124,55 @@ - [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` ,请按照顺时针螺旋顺序,返回矩阵中的所有元素。 @@ -132,6 +181,106 @@ - [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` 表示若干个区间的集合,合并所有重叠的区间,并返回一个不重叠的区间数组。 @@ -685,6 +834,89 @@ function bfs(grid, startI, startJ) { | [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) @@ -1151,3 +1383,157 @@ function partition(nums, left, right) { > 已有方法 `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; +} +``` From 17b0b1597ecd58c5f1944a195b5b5b5164c738d1 Mon Sep 17 00:00:00 2001 From: mpbfx <1835886801@qq.com> Date: Thu, 15 Jan 2026 23:19:48 +0800 Subject: [PATCH 13/17] feat: 1/15 --- .../\351\223\276\350\241\250.md" | 112 +++++++++++++++++- "\351\224\231\351\242\230\346\234\254.md" | 85 +++++++++++++ 2 files changed, 196 insertions(+), 1 deletion(-) create mode 100644 "\351\224\231\351\242\230\346\234\254.md" 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 4c8f318..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 @@ -## 链表 + ## 链表 ### 核心点 @@ -392,6 +392,32 @@ var detectCycle = function(head) { }; ``` +##### [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 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; +}; +``` + ##### [234. 回文链表](https://leetcode-cn.com/problems/palindrome-linked-list/) 请判断一个链表是否为回文链表。 @@ -550,6 +576,90 @@ var reverseKGroup = function(head, k) { }; ``` +##### [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; +}; +``` + ## 练习 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; +} +``` From 15667b66c7777871aae8b697d510995eccf050b0 Mon Sep 17 00:00:00 2001 From: mpbfx <1835886801@qq.com> Date: Thu, 15 Jan 2026 23:20:02 +0800 Subject: [PATCH 14/17] feat: 1/15 --- test.js | 22 ++-------------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/test.js b/test.js index 9ad1ae7..b7c7eee 100644 --- a/test.js +++ b/test.js @@ -1,21 +1,3 @@ -var reverseKGroup = (head, k) => { - let cur = head; - let count = 0; - while (count !== k && cur !== null) { - 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 = pre; - pre = node; - node = next; - } - head.next = reverseKGroup(cur, k); - return pre; - } - return head; +var genericSolution = function(head){ + } \ No newline at end of file From b257882ac00849863bd1eb1e4d68e4ac20138399 Mon Sep 17 00:00:00 2001 From: mpbfx Date: Tue, 20 Jan 2026 00:58:54 +0800 Subject: [PATCH 15/17] feat: 1/19 --- anki_binarytree.txt | 14 ++ anki_linkedlist.csv | 16 ++ anki_linkedlist.txt | 16 ++ generate_anki.js | 287 ++++++++++++++++++++++++++++++++++++ generate_anki_binarytree.js | 282 +++++++++++++++++++++++++++++++++++ test.js | 152 ++++++++++++++++++- 6 files changed, 765 insertions(+), 2 deletions(-) create mode 100644 anki_binarytree.txt create mode 100644 anki_linkedlist.csv create mode 100644 anki_linkedlist.txt create mode 100644 generate_anki.js create mode 100644 generate_anki_binarytree.js 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;
};
```
+

🌳 94. 二叉树的中序遍历

Easy

给你二叉树的根节点 `root` ,返回它节点值的中序遍历。

📎 LeetCode 链接

💡 解题代码

暂无代码答案,请参考 LeetCode 官方题解

+

🌳 101. 对称二叉树

Easy

给你一个二叉树的根节点 `root` ,检查它是否轴对称。

📎 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;
};
```
+

🌳 剑指 Offer 26. 树的子结构

Medium

输入两棵二叉树A和B,判断B是不是A的子结构。

📎 LeetCode 链接

💡 解题代码

```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;
};
```
+

🔗 21. 合并两个有序链表

Easy

将两个升序链表合并为一个新的升序链表并返回。

📎 LeetCode 链接

💡 解题代码

📝 解法 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 官方题解

+

🔗 141. 环形链表

Easy 判圈

给你一个链表的头节点 `head` ,判断链表中是否有环。

📎 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 index b7c7eee..7bed32d 100644 --- a/test.js +++ b/test.js @@ -1,3 +1,151 @@ -var genericSolution = function(head){ - +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; } \ No newline at end of file From 587065255826878fb99a7757e9df403b69910933 Mon Sep 17 00:00:00 2001 From: mpbfx Date: Tue, 20 Jan 2026 23:42:24 +0800 Subject: [PATCH 16/17] feat: 1/20 --- test.js | 289 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 289 insertions(+) diff --git a/test.js b/test.js index 7bed32d..b5ed062 100644 --- a/test.js +++ b/test.js @@ -148,4 +148,293 @@ var kGroup3 = (head, 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; } \ No newline at end of file From d55eab8af5f78a8d71f88d27f974db258cec740a Mon Sep 17 00:00:00 2001 From: mpbfx Date: Thu, 22 Jan 2026 00:33:17 +0800 Subject: [PATCH 17/17] feat: 1/20 --- test.js | 133 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) diff --git a/test.js b/test.js index b5ed062..6ab9106 100644 --- a/test.js +++ b/test.js @@ -437,4 +437,137 @@ const intersaction1 = (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