From 4d5cac9cdace26f50d31932c836b930d5a016ee8 Mon Sep 17 00:00:00 2001 From: realDuang <250407778@qq.com> Date: Mon, 2 Mar 2026 17:39:29 +0800 Subject: [PATCH] docs: merge bst topic into tree guide and refine structure --- README.md | 37 ++- docs/docs/topic/2.tree.md | 384 ++++++++++++------------ docs/docs/topic/3.binary-search-tree.md | 39 --- docs/index.md | 35 ++- 4 files changed, 234 insertions(+), 261 deletions(-) delete mode 100644 docs/docs/topic/3.binary-search-tree.md diff --git a/README.md b/README.md index a753bdf3a..564f9b5cf 100644 --- a/README.md +++ b/README.md @@ -18,25 +18,24 @@ LeetCode 题解仓库,收录 **300+ 道题目**的 JavaScript / TypeScript 解 ## 📖 专题目录 -以下 15 篇专题覆盖了 LeetCode 最核心的算法知识体系,建议按顺序阅读: - -| # | 专题 | 关键内容 | -| --- | --------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ | -| 0 | [前言](https://realduang.github.io/leetcode-in-javascript/docs/topic/0.introduction.html) | 写给工程师的算法学习观:提炼思想,拓展视野,而非做题家 | -| 1 | [重新认识递归](https://realduang.github.io/leetcode-in-javascript/docs/topic/1.recursive.html) | 递归的本质、思维方式与代码模板——一切高级算法的基石 | -| 2 | [二叉树遍历算法](https://realduang.github.io/leetcode-in-javascript/docs/topic/2.tree.html) | 前/中/后序遍历框架,理解"遍历"与"分解问题"两种思路 | -| 3 | [二叉搜索树](https://realduang.github.io/leetcode-in-javascript/docs/topic/3.binary-search-tree.html) | BST 的性质利用、增删查改操作框架 | -| 4 | [排序算法](https://realduang.github.io/leetcode-in-javascript/docs/topic/4.sort.html) | 经典排序算法对比与实现,理解分治与交换的思想 | -| 5 | [双指针问题](https://realduang.github.io/leetcode-in-javascript/docs/topic/5.two-pointers.html) | 快慢指针、左右指针、滑动窗口——线性结构问题的利器 | -| 6 | [二分搜索专题](https://realduang.github.io/leetcode-in-javascript/docs/topic/6.binary-search.html) | 统一二分搜索框架,彻底搞定边界问题 | -| 7 | [回溯问题](https://realduang.github.io/leetcode-in-javascript/docs/topic/7.backtrack.html) | 排列/组合/子集的通用回溯框架,一套模板解决一类问题 | -| 8 | [深度优先搜索 (DFS)](https://realduang.github.io/leetcode-in-javascript/docs/topic/8.depth-first-search.html) | 岛屿问题、连通分量、FloodFill 的 DFS 通解 | -| 9 | [广度优先搜索 (BFS)](https://realduang.github.io/leetcode-in-javascript/docs/topic/9.breadth-first-search.html) | 最短路径、层序遍历的 BFS 框架与变体 | -| 10 | [动态规划 - 问题推导](https://realduang.github.io/leetcode-in-javascript/docs/topic/10.dynamic-programming-normal.html) | DP 的思维推导过程:状态定义 → 转移方程 → 边界处理 | -| 11 | [动态规划 - 背包问题](https://realduang.github.io/leetcode-in-javascript/docs/topic/11.dynamic-programming-backpack.html) | 0-1 背包、完全背包的通用框架与空间优化 | -| 12 | [动态规划 - 子序列问题](https://realduang.github.io/leetcode-in-javascript/docs/topic/12.%20dynamic-programming-subsequence.html) | LCS、LIS 等经典子序列 DP 模型 | -| 13 | [图遍历算法](https://realduang.github.io/leetcode-in-javascript/docs/topic/13.graph.html) | 图的表示、遍历、环检测与拓扑排序 | -| 14 | [单调栈算法](https://realduang.github.io/leetcode-in-javascript/docs/topic/14.monotonic-stack.html) | 单调栈框架:下一个更大元素、柱状图等问题的通解 | +以下专题覆盖了 LeetCode 最核心的算法知识体系,建议按顺序阅读: + +| # | 专题 | 关键内容 | +| --- | --------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- | +| 0 | [前言](https://realduang.github.io/leetcode-in-javascript/docs/topic/0.introduction.html) | 写给工程师的算法学习观:提炼思想,拓展视野,而非做题家 | +| 1 | [重新认识递归](https://realduang.github.io/leetcode-in-javascript/docs/topic/1.recursive.html) | 递归的本质、思维方式与代码模板——一切高级算法的基石 | +| 2 | [二叉树遍历算法](https://realduang.github.io/leetcode-in-javascript/docs/topic/2.tree.html) | 前/中/后序遍历框架(含 BST 中序应用),理解"遍历"与"分解问题"两种思路 | +| 3 | [排序算法](https://realduang.github.io/leetcode-in-javascript/docs/topic/4.sort.html) | 经典排序算法对比与实现,理解分治与交换的思想 | +| 4 | [双指针问题](https://realduang.github.io/leetcode-in-javascript/docs/topic/5.two-pointers.html) | 快慢指针、左右指针、滑动窗口——线性结构问题的利器 | +| 5 | [二分搜索专题](https://realduang.github.io/leetcode-in-javascript/docs/topic/6.binary-search.html) | 统一二分搜索框架,彻底搞定边界问题 | +| 6 | [回溯问题](https://realduang.github.io/leetcode-in-javascript/docs/topic/7.backtrack.html) | 排列/组合/子集的通用回溯框架,一套模板解决一类问题 | +| 7 | [深度优先搜索 (DFS)](https://realduang.github.io/leetcode-in-javascript/docs/topic/8.depth-first-search.html) | 岛屿问题、连通分量、FloodFill 的 DFS 通解 | +| 8 | [广度优先搜索 (BFS)](https://realduang.github.io/leetcode-in-javascript/docs/topic/9.breadth-first-search.html) | 最短路径、层序遍历的 BFS 框架与变体 | +| 9 | [动态规划 - 问题推导](https://realduang.github.io/leetcode-in-javascript/docs/topic/10.dynamic-programming-normal.html) | DP 的思维推导过程:状态定义 → 转移方程 → 边界处理 | +| 10 | [动态规划 - 背包问题](https://realduang.github.io/leetcode-in-javascript/docs/topic/11.dynamic-programming-backpack.html) | 0-1 背包、完全背包的通用框架与空间优化 | +| 11 | [动态规划 - 子序列问题](https://realduang.github.io/leetcode-in-javascript/docs/topic/12.%20dynamic-programming-subsequence.html) | LCS、LIS 等经典子序列 DP 模型 | +| 12 | [图遍历算法](https://realduang.github.io/leetcode-in-javascript/docs/topic/13.graph.html) | 图的表示、遍历、环检测与拓扑排序 | +| 13 | [单调栈算法](https://realduang.github.io/leetcode-in-javascript/docs/topic/14.monotonic-stack.html) | 单调栈框架:下一个更大元素、柱状图等问题的通解 | > 📌 **推荐学习路径**:递归 → 二叉树 → 回溯 → DFS/BFS → 动态规划 → 其他专题 diff --git a/docs/docs/topic/2.tree.md b/docs/docs/topic/2.tree.md index 8741d907f..4bdee1e06 100644 --- a/docs/docs/topic/2.tree.md +++ b/docs/docs/topic/2.tree.md @@ -11,12 +11,12 @@ 前序在进入左右子节点之前处理;中序在遍历完左子树、准备遍历右子树之前处理;后序在左右子树都遍历完成后处理。 ```ts -function helper(node: TreeNode | null) { +function dfs(node: TreeNode | null) { if (!node) return; // 前序位置(进入节点时) - helper(node.left); + dfs(node.left); // 中序位置(左子树处理完,右子树处理前) - helper(node.right); + dfs(node.right); // 后序位置(离开节点时) } ``` @@ -24,7 +24,7 @@ function helper(node: TreeNode | null) { 若希望写成一个空间复杂度更低的非递归模板,可以直接套下面这个统一框架。和递归一样,只要在对应位置写逻辑即可: ```ts -function traverse(root: TreeNode) { +function dfs(root: TreeNode) { const stack: TreeNode[] = []; let node: TreeNode = root; @@ -66,6 +66,39 @@ function traverse(root: TreeNode) { 只写 `doPre` 就是前序,只写 `doIn` 就是中序,只写 `doPost` 就是后序;三者也可以组合使用。 +### BFS 遍历框架 + +BFS(层序遍历)的核心是队列:每次弹出一层节点,再把下一层节点入队。 + +```ts +function bfs(root: TreeNode | null): number[][] { + if (!root) return []; + + const res: number[][] = []; + const queue: TreeNode[] = [root]; + + while (queue.length) { + const level: number[] = []; + // 固定住当前层的节点数量,避免和下一层混在一起 + const len = queue.length; + + while(len--) { + 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; +} +``` + +如果题目是最短步数/最小深度这类最短路径问题,可以直接把 `res` 换成 `depth` 计数器:每处理完一层,`depth++`;首次命中目标就立刻返回。 + ### 前序、中序、后序的本质区别 理解三种遍历方式的**信息流方向**,是选择正确解法的关键: @@ -121,10 +154,10 @@ function traverse(root: TreeNode) { │ ── 组合与进阶 ── │ ├─ 需要构造一棵树? -│ └─ 是 → 分解问题思维(前序位置建根) ........ → 见「五」 +│ └─ 是 → 分解问题思维(按区间建根) .......... → 见「五」 │ -├─ 需要序列化/比较子树结构? -│ └─ 是 → 后序遍历 + 序列化 .................. → 见「六」 +├─ 需要对子树做签名比较/重复检测? +│ └─ 是 → 后序遍历 + 序列化签名 .............. → 见「六」 │ └─ 需要找最近公共祖先? └─ 是 → 后序遍历 + 分解问题 ................ → 见「七」 @@ -134,6 +167,8 @@ function traverse(root: TreeNode) { ## 一、前序遍历:自顶向下传递状态 +**判断关键词**:根到叶、路径记录、状态下传、回溯撤销。 + **适用场景**:需要从根节点出发,把状态/路径一路往下传递。 **思维模式**:遍历思维为主——在前序位置做选择,往下递归,到达叶节点时判断或收集结果。 @@ -143,38 +178,37 @@ function traverse(root: TreeNode) { 例如在 `[112] 路径总和` 中,从根节点出发,在前序位置将目标值减去当前节点值并传递给子节点,到达叶节点时判断剩余值是否为 0。 ```ts -function traverse(node: TreeNode | null, path: number[], target: number) { +function traverse(node: TreeNode | null, path: number[], remain: number) { if (!node) return; - // 前序位置:做选择 + // 关键1:前序做选择 path.push(node.val); + remain -= node.val; - // 到达叶节点,判断路径 - if (!node.left && !node.right) { - if (isValid(path, target)) { - res.push([...path]); - } - } + // 关键2:叶节点做判定/收集 + if (!node.left && !node.right && remain === 0) res.push([...path]); - traverse(node.left, path, target); - traverse(node.right, path, target); + traverse(node.left, path, remain); + traverse(node.right, path, remain); - // 离开时撤销选择(回溯) + // 关键3:回溯撤销 path.pop(); } ``` -题型参考: +题型参考(框架微调): -1. `[112] 路径总和` -2. `[113] 路径总和 II` -3. `[257] 二叉树的所有路径` -4. `[129] 求根节点到叶节点数字之和` -5. `[437] 路径总和 III` +| 题目 | 在前序框架上的微调点 | +| ---- | -------------------- | +| `[112] 路径总和` | 参数中携带剩余目标值,到叶节点判断是否为 0。 | +| `[113] 路径总和 II` | 在 `112` 基础上增加 `path` 数组,命中后拷贝路径入结果。 | +| `[257] 二叉树的所有路径` | 仍是路径下传,只是叶节点时把路径转成字符串。 | +| `[129] 求根节点到叶节点数字之和` | 路径状态从数组改成数字累积:`num = num * 10 + node.val`。 | +| `[437] 路径总和 III` | 单纯前序不够,通常改成“每个节点作为起点 + DFS”或前缀和。 | ### 1.2 结构判断与修改 -翻转、展平、判断相同/对称等操作,两种思维都可以使用。 +翻转、展平、判断对称等操作,两种思维都可以使用。 例如 `[226] 翻转二叉树`,每个节点要做的事就是交换左右子节点,放在前序位置(先交换再递归)或后序位置(先递归再组装)都能正确完成。 @@ -189,35 +223,67 @@ function invertTree(node: TreeNode | null): TreeNode | null { } ``` -```ts -// 分解问题思维:后序位置组装 -function invertTree(node: TreeNode | null): TreeNode | null { - if (!node) return null; - const left = invertTree(node.left); - const right = invertTree(node.right); - node.left = right; - node.right = left; - return node; -} -``` +后序写法也等价:先递归拿到 `left/right`,再在返回前交换组装。 -题型参考: +题型参考(框架微调): -1. `[226] 翻转二叉树` -2. `[572] 另一棵树的子树` -3. `[100] 相同的树` -4. `[101] 对称二叉树` +| 题目 | 在框架上的微调点 | +| ---- | ---------------- | +| `[226] 翻转二叉树` | 前序直接交换左右子树,或后序拿到左右结果后再组装。 | +| `[101] 对称二叉树` | 把“比较同位”改成“比较镜像位”(左左对右右变左对右)。 | +| `[114] 二叉树展开为链表` | 前序收集访问顺序后重连,或后序返回链表尾节点做拼接。 | ## 二、中序遍历:BST 有序操作 +**判断关键词**:BST、有序性、第 K 小、前驱后继、区间统计。 + **适用场景**:题目涉及 BST,且需要利用其有序性。 **核心原理**:BST 的中序遍历结果是有序的。 -例如在 `[230] 二叉搜索树中第K小的元素` 中,只需中序遍历到第 K 个节点即可。更多 BST 的内容见 [二叉搜索树](./3.binary-search-tree.md) 专题。 +这一部分是「二叉树遍历」专题里对 BST 的重点补充:聚焦中序有序性在解题中的直接应用,而不展开为独立的数据结构增删改查专题。 + +### 2.1 BST 的性质与中序有序性 + +二叉搜索树(BST)只需抓住两点: + +1. 对于任意节点 `node`,左子树所有值都小于 `node.val`,右子树所有值都大于 `node.val`。 +2. 任意节点的左右子树本身也都是 BST。 + +这两个特性直接推导出一个结论:**BST 的中序遍历结果一定有序**。所以涉及顺序统计(第 K 小、第 K 大、区间计数)时,中序遍历通常是首选。 + +例如在 `[230] 二叉搜索树中第K小的元素` 中,问题可以直接转化为“返回中序遍历的第 `k` 个节点值”: + +```ts +function kthSmallest(root: TreeNode | null, k: number): number { + let rank = 0; + let ans = 0; + + function dfs(node: TreeNode | null): void { + if (!node || rank >= k) return; // 关键:可提前退出 + dfs(node.left); + if (++rank === k) ans = node.val; + dfs(node.right); + } + + dfs(root); + return ans; +} +``` + +题型参考(框架微调): + +| 题目 | 在 BST 框架上的微调点 | +| ---- | --------------------- | +| `[230] 二叉搜索树中第K小的元素` | 中序遍历 + 计数器,命中第 `k` 个后提前退出。 | +| `[98] 验证二叉搜索树` | 中序时维护 `prev` 保证严格递增,或用上下界递归。 | +| `[700] 二叉搜索树中的搜索` | 不必完整遍历,按值大小只走一侧子树。 | +| `[235] 二叉搜索树的最近公共祖先` | 利用大小关系剪枝:同在左/右则下探,否则当前即答案。 | ## 三、后序遍历:自底向上汇总结果 +**判断关键词**:子树结果先算、返回值汇总、平衡/直径/路径和。 + **适用场景**:当前节点的答案需要由左右子树的结果推导。这是二叉树中最常见的模式。 **思维模式**:分解问题思维为主——递归函数有明确返回值,在后序位置利用左右子树的返回值计算当前节点的结果。 @@ -228,50 +294,30 @@ function invertTree(node: TreeNode | null): TreeNode | null { 例如在 `[104] 二叉树的最大深度` 中,当前节点的深度 = max(左子树深度, 右子树深度) + 1;在 `[222] 完全二叉树的节点个数` 中,节点数 = 左子树节点数 + 右子树节点数 + 1。 -**解题公式**:当前节点的属性 = f(左子树属性, 右子树属性, 当前节点值) +**解题公式**:当前节点属性 = `f(left, right, node)` ```ts -function property(node: TreeNode | null): number { - if (!node) return baseCase; +function solve(node: TreeNode | null): number { + // 返回“当前子树的核心属性”(如高度) + // base case + if (!node) return 0; - const left = property(node.left); - const right = property(node.right); - - // 后序位置:利用左右子树的结果计算当前节点的属性 - return f(left, right, node.val); -} -``` + const left = solve(node.left); + const right = solve(node.right); -示例 — 最大深度:`f = Math.max(left, right) + 1`,`baseCase = 0` + // 可选:用 left/right 更新全局答案(例如直径、平衡性、路径最值) + // updateGlobal(left, right, node.val); -示例 — 节点总数:`f = left + right + 1`,`baseCase = 0` - -示例 — 二叉树直径(需要全局变量辅助): - -```ts -function diameterOfBinaryTree(root: TreeNode | null): number { - let maxDiameter = 0; - - function depth(node: TreeNode | null): number { - if (!node) return 0; - const left = depth(node.left); - const right = depth(node.right); - // 后序位置更新全局最大直径 - maxDiameter = Math.max(maxDiameter, left + right); - return Math.max(left, right) + 1; - } - - depth(root); - return maxDiameter; + // 返回给父节点的值 + return Math.max(left, right) + 1; } ``` -题型参考: +示例映射: -1. `[104] 二叉树的最大深度` -2. `[543] 二叉树的直径` -3. `[110] 平衡二叉树` -4. `[222] 完全二叉树的节点个数` +1. 最大深度:返回高度。 +2. 节点总数:返回 `left + right + 1`。 +3. 二叉树直径:返回高度,同时用 `left + right` 更新全局最大直径。 ### 3.2 任意路径最值 @@ -281,95 +327,74 @@ function diameterOfBinaryTree(root: TreeNode | null): number { ```ts function maxPathSum(root: TreeNode | null): number { - let maxSum = -Infinity; + let best = -Infinity; - function maxGain(node: TreeNode | null): number { + function gain(node: TreeNode | null): number { if (!node) return 0; - // 负增益的路径直接丢弃 - const left = Math.max(0, maxGain(node.left)); - const right = Math.max(0, maxGain(node.right)); - - // 后序位置:尝试用双边路径更新全局最优 - maxSum = Math.max(maxSum, left + right + node.val); - - // 返回单边最大路径(供父节点使用) - return Math.max(left, right) + node.val; + const left = Math.max(0, gain(node.left)); + const right = Math.max(0, gain(node.right)); + best = Math.max(best, left + right + node.val); // 双边更新全局 + return Math.max(left, right) + node.val; // 单边返回父节点 } - maxGain(root); - return maxSum; + gain(root); + return best; } ``` -题型参考: +题型参考(框架微调): -1. `[124] 二叉树中的最大路径和` -2. `[543] 二叉树的直径` +| 题目 | 在后序框架上的微调点 | +| ---- | -------------------- | +| `[104] 二叉树的最大深度` | 返回值定义为“子树高度”,公式是 `max(left, right) + 1`。 | +| `[110] 平衡二叉树` | 返回高度时附带平衡性判断;常用 `-1` 作为失衡哨兵提前剪枝。 | +| `[222] 完全二叉树的节点个数` | 通用后序可解,进阶可利用完全树性质按高度优化。 | +| `[124] 二叉树中的最大路径和` | 返回单边贡献,双边路径只用于更新全局最优。 | +| `[543] 二叉树的直径` | 与 `124` 同型:返回单边高度,统计双边路径长度。 | ## 四、BFS 层序遍历 -**适用场景**:涉及逐层处理、层级关系、最短路径。 - -除 DFS 外,二叉树还可以用 BFS 逐层处理节点。核心数据结构是**队列**:每次从队头取出节点处理,再将其子节点入队。 - -例如在 `[102] 二叉树的层序遍历` 中,通过在每轮循环开始时记录当前队列长度(即本层节点数),就能精确地将每一层的节点分开处理。 - -### 代码模板 - -```ts -function levelOrder(root: TreeNode | null): number[][] { - if (!root) return []; - const res: number[][] = []; - const queue: TreeNode[] = [root]; - - while (queue.length > 0) { - const levelSize = queue.length; - const level: number[] = []; - - for (let i = 0; i < levelSize; i++) { - const node = queue.shift()!; - level.push(node.val); - node.left && queue.push(node.left); - node.right && queue.push(node.right); - } +**判断关键词**:逐层处理、最短步数、最小深度、队列扩散。 - res.push(level); - } - return res; -} -``` +**适用场景**:涉及逐层处理、层级关系、最短路径。 -### 变体 +标准代码模板见「前置知识 -> BFS 遍历框架」。本节不再重复模板,只关注如何按题意改造。 -代码框架几乎固定,面试中的变化只在于**如何处理每一层的数据**: +### 常见改造点 -| 变体 | 处理方式 | -| --------------------- | ---------------------------------- | -| 标准层序遍历(102) | 每层收集到一个子数组 | -| 锯齿形层序遍历(103) | 奇数层正序,偶数层反转 | -| 右视图(199) | 每层只取最后一个元素 | -| 每层最大值(515) | 每层取 max | -| 最小深度(111) | 遇到第一个叶节点即返回当前层数 | -| 填充右侧指针(117) | 利用层内前后节点关系连接 next 指针 | +| 题型 | 在骨架中的改动点 | +| --------------------- | ------------------------------------------------ | +| 标准层序遍历(102) | 每层结束 `res.push(level)` | +| 锯齿形层序遍历(103) | 按层号决定 `level` 是否反转后再入 `res` | +| 右视图(199) | 每层取最后一个访问节点值 | +| 每层最大值(515) | 每层循环中维护 `levelMax` | +| 最小深度(111) | 增加 `depth`;遇到首个叶节点立即返回 | +| 填充右侧指针(117) | 层内记录 `prev`,将 `prev.next = node` 逐个连接 | -BFS 多用于求最小高度、最短路径等场景,通常不需遍历完整棵树即可得出答案。 +BFS 的优势在于“按距离扩散”,所以最短路径/最小步数类问题通常优先考虑 BFS。 -题型参考: +题型参考(框架微调): -1. `[102] 二叉树的层序遍历` -2. `[103] 二叉树的锯齿形层序遍历` -3. `[107] 二叉树的层序遍历 II` -4. `[199] 二叉树的右视图` -5. `[515] 在每个树行中找最大值` -6. `[111] 二叉树的最小深度` -7. `[117] 填充每个节点的下一个右侧节点指针 II` +| 题目 | 在 BFS 骨架上的微调点 | +| ---- | --------------------- | +| `[102] 二叉树的层序遍历` | 每层收集 `level`,层结束后 `res.push(level)`。 | +| `[103] 二叉树的锯齿形层序遍历` | 按层号决定当前层正序还是反序。 | +| `[107] 二叉树的层序遍历 II` | 正常层序后反转结果,或改为头插结果。 | +| `[199] 二叉树的右视图` | 每层取最后一个弹出的节点值。 | +| `[515] 在每个树行中找最大值` | 层内维护 `levelMax`,层结束写入结果。 | +| `[111] 二叉树的最小深度` | 增加 `depth` 计数,首次遇到叶节点立即返回。 | +| `[117] 填充每个节点的下一个右侧节点指针 II` | 层内维护 `prev` 指针,顺序连接 `prev.next = cur`。 | --- ## 五、构造类 +**判断关键词**:由序列建树、确定根节点、划分左右区间。 + **适用场景**:从遍历序列或其他条件构造一棵二叉树。 +这一章是分解问题思维在“反向建树”场景的应用,不再重复递归遍历基础框架。 + **思维模式**:分解问题思维——构造整棵树 = 确定根节点 + 递归构造左子树 + 递归构造右子树。 例如在 `[654] 最大二叉树` 中,根节点就是数组中的最大值,最大值左侧构成左子树,右侧构成右子树。所有构造类题目都遵循这一模式,差异仅在于"如何确定根节点"和"如何划分左右子树的范围"。 @@ -421,57 +446,50 @@ function build( **优化技巧**:使用 HashMap 存储中序遍历的 `value → index` 映射,避免每次递归都线性查找。 -题型参考: +题型参考(框架微调): -1. `[105] 从前序与中序遍历序列构造二叉树` -2. `[106] 从中序与后序遍历序列构造二叉树` -3. `[889] 根据前序和后序遍历构造二叉树` -4. `[654] 最大二叉树` +| 题目 | 在构造框架上的微调点 | +| ---- | -------------------- | +| `[105] 从前序与中序遍历序列构造二叉树` | 根取前序首元素,在中序中定位根并按左子树大小切分区间。 | +| `[106] 从中序与后序遍历序列构造二叉树` | 根取后序尾元素,其余划分逻辑与 `105` 对称。 | +| `[889] 根据前序和后序遍历构造二叉树` | 用前序第二个元素定位左子树范围,但结果通常不唯一。 | +| `[654] 最大二叉树` | 对照题:不依赖遍历序列,根由“当前区间最大值”直接确定。 | ## 六、序列化与子树比较 +**判断关键词**:子树判等、重复子树、序列化签名、哈希统计。 + **适用场景**:判断两棵树是否相同、是否为子树、寻找重复子树。 +本章只讲“序列化签名”技巧;后序遍历的基础模板与返回值思路见第三章。 + **思维模式**:分解问题思维——在后序位置将每棵子树序列化为字符串,用 HashMap 记录出现次数。 -要拿到一棵子树的完整字符串表示,必须先拿到左右子树的序列化结果再拼上当前节点——这天然就是后序遍历的操作位置。 +要拿到一棵子树的完整表示,必须先拿到左右子树结果再拼上当前节点,所以天然是后序位置。 ```ts -function findDuplicateSubtrees(root: TreeNode | null): TreeNode[] { - const res: TreeNode[] = []; - const memo = new Map(); - - function serialize(node: TreeNode | null): string { - if (!node) return '#'; - - const left = serialize(node.left); - const right = serialize(node.right); - - // 后序位置:拿到左右子树的序列化结果,组装当前子树 - const subTree = `${left},${right},${node.val}`; - - const count = memo.get(subTree) ?? 0; - if (count === 1) { - res.push(node); - } - memo.set(subTree, count + 1); - - return subTree; - } - - serialize(root); - return res; -} +// 伪代码:后序序列化 + 哈希计数 +serialize(node): + if node == null: return "#" + left = serialize(node.left) + right = serialize(node.right) + sig = left + "," + right + "," + node.val + if count(sig) == 1: collect(node) + count(sig) += 1 + return sig ``` -题型参考: +题型参考(框架微调): -1. `[100] 相同的树` -2. `[572] 另一棵树的子树` -3. `[297] 二叉树的序列化与反序列化` +| 题目 | 在序列化/比较框架上的微调点 | +| ---- | --------------------------- | +| `[572] 另一棵树的子树` | 主树每个节点都可作为候选根,做一次子树相同判断或签名匹配。 | +| `[297] 二叉树的序列化与反序列化` | 关注编码与解码互逆;先定遍历顺序,再处理空节点占位。 | ## 七、最近公共祖先(LCA) +**判断关键词**:两个目标节点、公共祖先、左右子树各命中一个。 + **适用场景**:寻找两个节点的最近公共祖先。 **思维模式**:分解问题思维——在后序位置,一个节点能知道左右子树中是否包含目标节点。 @@ -506,14 +524,10 @@ function lowestCommonAncestor(root: TreeNode | null, p: TreeNode, q: TreeNode): ```ts function lowestCommonAncestor(root: TreeNode | null, p: TreeNode, q: TreeNode): TreeNode | null { + // 关键:利用 BST 有序性只走一边 if (!root) return null; - - if (p.val < root.val && q.val < root.val) { - return lowestCommonAncestor(root.left, p, q); - } - if (p.val > root.val && q.val > root.val) { - return lowestCommonAncestor(root.right, p, q); - } + if (p.val < root.val && q.val < root.val) return lowestCommonAncestor(root.left, p, q); + if (p.val > root.val && q.val > root.val) return lowestCommonAncestor(root.right, p, q); return root; } ``` @@ -530,6 +544,6 @@ function lowestCommonAncestor(root: TreeNode | null, p: TreeNode, q: TreeNode): | 属性计算(深度/节点数/直径) | 后序 | 分解问题 | 后序位置组合左右结果 | | 任意路径最值 | 后序 | 分解问题 | 后序位置更新全局最优 | | 逐层处理/最短路径 | BFS | 遍历 | 每层循环处理 | -| 构造二叉树 | 前序 | 分解问题 | 前序位置创建根节点 | -| 子树比较/序列化 | 后序 | 分解问题 | 后序位置序列化子树 | +| 构造二叉树(反向建树) | 递归分解 | 分解问题 | 按区间确定根并划分左右子树 | +| 子树签名比较/重复检测 | 后序 | 分解问题 | 后序位置生成序列化签名 | | 最近公共祖先 | 后序 | 分解问题 | 后序位置判断左右结果 | diff --git a/docs/docs/topic/3.binary-search-tree.md b/docs/docs/topic/3.binary-search-tree.md deleted file mode 100644 index 59f005a58..000000000 --- a/docs/docs/topic/3.binary-search-tree.md +++ /dev/null @@ -1,39 +0,0 @@ -# 二叉搜索树 - -二叉搜索树,即 BST (Binary Search Tree)。我们先来看看它的定义: - -> 二叉搜索树是一棵空树,或者是具有下列性质的二叉树: -> 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值; -> 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值。 - -从它的定义中,我们可以总结出它拥有的两个特性: - -1. 对于 BST 的每一个节点 node,左子树节点的值都比 node 的值要小,右子树节点的值都比 node 的值大。 -2. 对于 BST 的每一个节点 node,它的左侧子树和右侧子树都是 BST。 - -这是一个非常重要的数据结构设计。经过自平衡优化的 BST,如红黑树,AVL树,能够从增删改查各个方面全部实现 O(lgN) 级别的时间复杂度。 - -从它的两个特性我们可以注意到,它与我们在快速排序完成后的树形结构是一模一样的。也就是说,二叉搜索树的**中序遍历结果一定是有序的**。这个特性对我们解题的思路十分有帮助。 - -例如在 `[230] 二叉搜索树中第K小的元素` 中,我们需要在 BST 中找出第 K 小的元素。由于 BST 其中序遍历是有序的,因此题目就被直接简化成:输出中序遍历的第K个结果。题目就变得十分简单了。 - -```ts -function kthSmallest(root: TreeNode | null, k: number): number { - let count = 0; - let res = root.val; - traverse(root); - return res; - - function traverse(node: TreeNode | null) { - if (!node) return; - - traverse(node.left); - count += 1; - if (count === k) { - res = node.val; - return; - } - traverse(node.right); - } -} -``` diff --git a/docs/index.md b/docs/index.md index 9b68db6f1..0ef0d3d2b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -35,25 +35,24 @@ footer: false ## 📖 专题目录 -以下 15 篇专题覆盖了 LeetCode 最核心的算法知识体系,建议按顺序阅读: +以下专题覆盖了 LeetCode 最核心的算法知识体系,建议按顺序阅读: -| # | 专题 | 关键内容 | -| --- | ------------------------------------------------------------------- | -------------------------------- | -| 0 | [前言](/docs/topic/0.introduction) | 写给工程师的算法学习观 | -| 1 | [重新认识递归](/docs/topic/1.recursive) | 递归的本质、思维方式与代码模板 | -| 2 | [二叉树遍历算法](/docs/topic/2.tree) | 前/中/后序遍历框架 | -| 3 | [二叉搜索树](/docs/topic/3.binary-search-tree) | BST 的性质利用、增删查改框架 | -| 4 | [排序算法](/docs/topic/4.sort) | 经典排序算法对比与实现 | -| 5 | [双指针问题](/docs/topic/5.two-pointers) | 快慢指针、左右指针、滑动窗口 | -| 6 | [二分搜索专题](/docs/topic/6.binary-search) | 统一二分搜索框架 | -| 7 | [回溯问题](/docs/topic/7.backtrack) | 排列/组合/子集的通用回溯框架 | -| 8 | [深度优先搜索](/docs/topic/8.depth-first-search) | 岛屿问题、连通分量的 DFS 通解 | -| 9 | [广度优先搜索](/docs/topic/9.breadth-first-search) | 最短路径、层序遍历的 BFS 框架 | -| 10 | [动态规划 - 推导](/docs/topic/10.dynamic-programming-normal) | 状态定义 → 转移方程 → 边界处理 | -| 11 | [动态规划 - 背包](/docs/topic/11.dynamic-programming-backpack) | 0-1 背包、完全背包的通用框架 | -| 12 | [动态规划 - 子序列](/docs/topic/12.dynamic-programming-subsequence) | LCS、LIS 等经典子序列 DP 模型 | -| 13 | [图遍历算法](/docs/topic/13.graph) | 图的表示、遍历、环检测与拓扑排序 | -| 14 | [单调栈算法](/docs/topic/14.monotonic-stack) | 下一个更大元素、柱状图的通解 | +| # | 专题 | 关键内容 | +| --- | ------------------------------------------------------------------- | ------------------------------------- | +| 0 | [前言](/docs/topic/0.introduction) | 写给工程师的算法学习观 | +| 1 | [重新认识递归](/docs/topic/1.recursive) | 递归的本质、思维方式与代码模板 | +| 2 | [二叉树遍历算法](/docs/topic/2.tree) | 前/中/后序遍历框架(含 BST 中序应用) | +| 3 | [排序算法](/docs/topic/4.sort) | 经典排序算法对比与实现 | +| 4 | [双指针问题](/docs/topic/5.two-pointers) | 快慢指针、左右指针、滑动窗口 | +| 5 | [二分搜索专题](/docs/topic/6.binary-search) | 统一二分搜索框架 | +| 6 | [回溯问题](/docs/topic/7.backtrack) | 排列/组合/子集的通用回溯框架 | +| 7 | [深度优先搜索](/docs/topic/8.depth-first-search) | 岛屿问题、连通分量的 DFS 通解 | +| 8 | [广度优先搜索](/docs/topic/9.breadth-first-search) | 最短路径、层序遍历的 BFS 框架 | +| 9 | [动态规划 - 推导](/docs/topic/10.dynamic-programming-normal) | 状态定义 → 转移方程 → 边界处理 | +| 10 | [动态规划 - 背包](/docs/topic/11.dynamic-programming-backpack) | 0-1 背包、完全背包的通用框架 | +| 11 | [动态规划 - 子序列](/docs/topic/12.dynamic-programming-subsequence) | LCS、LIS 等经典子序列 DP 模型 | +| 12 | [图遍历算法](/docs/topic/13.graph) | 图的表示、遍历、环检测与拓扑排序 | +| 13 | [单调栈算法](/docs/topic/14.monotonic-stack) | 下一个更大元素、柱状图的通解 | ## 📂 题解分类