【算法】树(三):BST 与中序——迭代三兄弟、立起来的二分,与双通道的满血收官
【算法】树(三):BST 与中序——迭代三兄弟、立起来的二分,与双通道的满血收官 摘要 树系列第三篇,也是收官篇。上半场 BST 子家族:LC98 凭记忆重写一次过(中序 + prev + 短路,验收带"父子全合法但整体不是 BST"的著名陷阱用例);LC230 第 K 小三版弧线——k-- 长错位置(动作位置课)→ 全量收集(被正名为对拍参照)→ 迭代版提前停(“栈在谁手里,停下的方式就是谁的风
文章信息
- 原文链接:https://jiayq.blog.csdn.net/article/details/166132971
- 发布时间:2026-09-21 09:00:00
- 标签:#Golang, #算法, ##开学季·九月创作之星博客挑战赛, ##后序, ##BST
【算法】树(三):BST 与中序——迭代三兄弟、立起来的二分,与双通道的满血收官
摘要
树系列第三篇,也是收官篇。上半场 BST 子家族 :LC98 凭记忆重写一次过(中序 + prev + 短路,验收带”父子全合法但整体不是 BST”的著名陷阱用例);LC230 第 K 小 三版弧线——k-- 长错位置(动作位置课)→ 全量收集(被正名为对拍参照)→ 迭代版提前停(“栈在谁手里,停下的方式就是谁的风格”)。中序的迭代写法引出支线迭代三兄弟 :前序弹即处理、中序左链循环、后序窥视记账——递归版三个遍历挪一行,迭代版是三个骨架,难度全在”中”的位置上;我在前序改造里 panic(空栈取 [-1])、后序改造里死循环(节点 1 永远不被处理),两次翻车都成了教材。LC235 BST 版 LCA :一句自己冒出来的观察——“BST 是立起来的二分 ”——把二分系列和树系列合龙;外加性质换复杂度的单向门、迭代版”else 吸收等号”的小机锋。下半场两道收官题:LC105 前序+中序构造二叉树 三版弧线——孤儿节点(创建了没挂载)、fail-silent(防御性边界静默吞掉顶层调用错误)、闭区间纪律(len-1 和 <=inR 错一个字符树就镜像);LC124 最大路径和 (hard)——双通道的满血版:六候选单通道的”分叉路径”死法、nil 哨兵撞负值、接收端剪枝、-MaxInt 初始化。外加教练翻车第三案:我出的 want 标错了,错的恰好是本题考点——单节点也是路径 。
前置阅读:树(一):递归的艺术——分解与遍历、两通道与死亡信号、树(二):队列与祖先——BFS 骨架三配件、短路透传与合流。配套代码仓库(按题号分目录):https://github.com/a18792721831/studyleetCode
1. LC98:凭记忆重写的验收
98 之前做过,这次是凭记忆重写 (这件事本身是验收的一部分——三个要素:中序、prev、短路,全都在,说明”BST ⟺ 中序严格递增”已经内化):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
prev := -math.MaxInt
var inorder func(r *TreeNode) bool
inorder = func(r *TreeNode) bool {
if r == nil {
return true
}
if !inorder(r.Left) { // 左子树失败,短路
return false
}
if prev >= r.Val { // ★ 中序位置:必须严格递增(等号也不行)
return false
}
prev = r.Val
return inorder(r.Right) // 右子树失败,短路
}
验收用例里最值钱的是这个著名陷阱:
1
2
3
4
5
6
7
5
/ \
4 6
/ \
3 7 ← 每个"父子对"都合法(4<5, 3<6<7)
但 3 在 5 的右子树里,必须 > 5 —— 不是 BST
只比较父子不够,中序 prev 一刀切干净 :3 出现在 5 之后,prev(5) >= 3 立即判死。另有两处细节:prev 初值 -math.MaxInt 低于题目 int32 下界(单节点取到最小值也安全)——树(一)哨兵值域课的再现;短路返回让最坏情况也不用走完整棵树。
2. LC230 三版:动作位置 → 对拍参照 → 提前停
BST 中第 k 小的值。进阶:如果树被频繁修改,能否优化。
2.1 第一版:k– 长错了位置
第一版的病:k-- 长在函数入口 (前序位置)——计数先于”到达中序位置”,遍历次序整个错位。修复版的形态:
1
2
3
4
inorder(ro.Left) // ① 先走左
q = append(q, ro.Val) // ② 中序位置:值在这里入队
inorder(ro.Right) // ③ 再走右
这题的教训后来在迭代三兄弟(§3)里升格成通课:中序的”中”不是一个时刻,是一个位置——左递归之后、右递归之前 。动作长在哪个位置,决定了你做的是前序/中序/后序里的哪一种。
2.2 第二版:全量收集,被正名为对拍参照
第二版是全量收集:中序遍历整棵树塞进 q,返回 q[k-1]。功能全对,但 k=1 也要把 10 万个节点走完——没有提前停。
批改时给它正了名:这是对拍参照的标准件 。树(二)里记过教练翻车案(build 建错树、want 标错),解法就是”写一个傻但不会错的参照实现对拍”——逻辑简单到不会错的版本,拿它和优化版随机对跑,优化版有 bug 当场暴露。push 时两版都留,参照版标注 // 对拍参照:O(n) 全量收集。
2.3 第三版:迭代提前停
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
stack := []*TreeNode{}
cur := root
for len(stack) > 0 || cur != nil {
for cur != nil { // ① 一路向左,全部压栈
stack = append(stack, cur)
cur = cur.Left
}
cur = stack[len(stack)-1] // ② 弹出栈顶 = 当前中序节点
stack = stack[:len(stack)-1]
k--
if k == 0 {
return cur.Val // ← 裸 return:栈在你手里,提前停就是 return
}
cur = cur.Right // ③ 转向右子树
}
递归版提前停要靠全局 k 剪枝”通知”还在调用栈里的祖先;迭代版直接 return——栈在谁手里,停下的方式就是谁的风格 。
3. 支线:迭代三兄弟——递归挪一行,迭代换骨架
做迭代版 230 时冒出一个问题:改成前序/后序呢?我在这里连翻两次车,翻得值回票价。
3.1 前序:弹即处理(两次翻车现场)
第一翻:把”弹栈处理”挪到循环开头(想着前序”中”最先),第一次进循环栈还是空的——stack[-1] 直接 panic。中序版弹栈安全是”先压栈后弹”的顺序给的保证,调换顺序就把保证拆了 。
第二翻更隐蔽:修好初始栈后,弹出处理的节点又在压栈循环里被压回去——每个节点处理两遍 。中序版弹栈和压栈处理的是不同节点(弹的是”回访的祖先”,压的是”新发现的左链”),我的版本同一个节点两头都占。
正解其实比中序更简单 ——前序的”中”在最前,没有延迟,不需要左链循环:
1
2
3
4
5
6
7
8
9
stack := []*TreeNode{root}
for len(stack) > 0 {
cur := stack[len(stack)-1]
stack = stack[:len(stack)-1]
// 处理 cur(前序位置:弹出即处理)
if cur.Right != nil { stack = append(stack, cur.Right) } // 右先压
if cur.Left != nil { stack = append(stack, cur.Left) } // 左后压 → 左先出(LIFO)
}
压子顺序反着写 (右先左后),栈的后进先出让左先出——正好”中左右”。
3.2 中序:两本账
回头看中序的双循环条件,拷问三个子问题:什么时候栈空、cur 非 nil?什么时候栈非空、cur 为 nil?什么时候两者全灭?拿右链树 1→2→3 走一遍:
1
2
3
4
5
6
7
8
动作 栈 cur
初始 [] 1
压1,左走到底 [1] nil
弹1,处理,转右 [] 2 ← 栈空了!树才走 1/3
压2,左走到底 [2] nil
弹2,处理,转右 [] 3 ← 栈又空了
弹3,处理,转右 [] nil ← 现在才是走完:两者全灭
栈空了三次,前两次都靠cur != nil 续命。双条件的本质是两本账:
1
2
3
4
len(stack) > 0 账本一:已入栈、未处理(祖先链)
cur != nil 账本二:已发现、未入栈(右子树入口)
循环继续 = 还有任何一笔账没结
处理(弹出)消耗账本一,移动(转右)生产账本二——缺一本就赖账丢节点 。
3.3 后序:窥视 + 记账(死循环解剖)
后序”左右中”的难点:“中”的处理要跨越整个右子树的完成 。我的改造版实测死循环——树 1←2 找 k=2 返回 2 而不是 1(节点 1 永远不被处理)。三大死法:
- 处理完的节点没移走 :
k--之后 cur 还指着它,下一轮压栈循环又把它压回去——重复处理,栈底的 1 永远轮不到弹出 - “中”节点永久丢失 :弹出即出栈,若它有右子树,压的是孩子——但自己已经不在栈里,右子树走完没人回来处理它
- 右链循环不回炉 :
for cur.Right != nil只往右钻,右子树自己的左链整棵蒸发(我在注释里预感到了”如果右节点比左节点长呢”,但没追问下去)
病根一句话:栈里的节点分不清”右子树没走”和”右子树走完了”,这个信息必须额外记账 :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var last *TreeNode // 上一个处理完的节点
for len(stack) > 0 || cur != nil {
for cur != nil { // 左链压栈(和前两个遍历一样)
stack = append(stack, cur)
cur = cur.Left
}
top := stack[len(stack)-1] // ★ 窥视,不弹出!
if top.Right != nil && top.Right != last {
cur = top.Right // 右子树没走完 → 转右,top 留在栈里等
} else {
stack = stack[:len(stack)-1] // 右子树走完/不存在 → 现在才弹出
// 处理 top(后序位置)
last = top // ★ 记账:它处理完了
}
}
两处和中序的骨架级差异 :中序的弹是确定的(弹即处理),后序的弹是有条件的 ——右子树没走完,top 必须留在栈里。last == top.Right ⟺ 右子树已完整走完——为什么看一个指针就够?右子树的后序遍历,最后处理的节点恰好是右子树的根 (后序根在最后)。”上一个处理的是 top.Right”就是”右子树全部完成”的铁证。
巧解一条:前序骨架把压子顺序对调 (左先压右后压 → 右先出),得”中右左”,整体反转就是”左右中”。
3.4 三兄弟的难度阶梯
1
2
3
4
前序:弹即处理 —— 最简单,无延迟
中序:延迟处理 —— 左链循环 + cur = cur.Right 移走
后序:条件弹 + 记账 —— 窥视不弹出 + lastVisited
递归版三个遍历只挪一行(动作位置),迭代版是三个骨架 ——难度全在”中”的位置上:中在前无延迟,中在中间延迟到右子树开始前,中在最后延迟到右子树完成后。递归的优雅统一(编译器替你管调用栈)在迭代版失效:栈自己拿,三种时机就是三种结构。
4. LC235:立起来的二分
BST 找 p、q 的最近公共祖先。
4.1 一句观察,两个系列合龙
做题前的闲聊里冒出一句话:”相当于是树形的二分了 。”展开成对照表:
| 数组二分 | LC235 | |
|---|---|---|
| 每步看谁 | mid | root |
| 比一次,砍掉 | 一半区间 | 一棵子树 |
| 前提 | 数组有序 | 中序遍历有序 (BST 的定义) |
| 复杂度 | O(log n) | O(h),平衡时 = O(log n) |
两边能互相转换:有序数组取中间元素当根、递归构造,得到平衡 BST——数组二分的 mid 序列,恰好就是那棵 BST 的 root 序列。数组二分 = 把 BST 压扁;BST 上的搜索 = 把二分立起来。
一处不对称要记住:数组二分的 mid 永远居中,BST 的 root 可以歪——树不平衡时 h > log n,BST 的”二分”最坏退化成顺序扫。AVL/红黑树拼命保平衡,保的不是好看,是二分的速度。
4.2 性质换复杂度:一扇单向门
236 的解法(后序递归+双通道)在 235 上能用吗?——能用,浪费:O(n) 全树探索 vs O(h) 方向判断。反过来 235 的解法能用在 236 上吗?——不能:”p、q 分居两侧”的判断依赖有序性 ,普通树上 p.Val < root.Val 不代表 p 在左子树。性质一撤,专用解立刻失效。
1
2
题目多给一个性质(有序) → 算法砍掉一层复杂度(n → h)
二分系列整季都在做这笔交易(有序数组找值:线性 O(n) / 二分 O(log n));反向也成立——性质被偷偷撤走时,专用解原地爆炸 (LC81 之于 LC33:重复元素撤走”互不相同”)。
4.3 三行逻辑,两个小机锋
1
2
3
4
p、q 都 < root.Val → 都在左子树 → 往左
p、q 都 > root.Val → 都在右子树 → 往右
一大一小(或相等) → 分岔了!root 就是 LCA
分岔的合法性来自树(二)LC236 的判决书:LCA 必是分岔点 ——两人路径第一次汇合的位置;再往下走就只跟其中一人同路了。普通树里找分岔点要靠递归从叶往根汇报,BST 里分岔点的值域特征直接写在 root 身上 。
递归版一版过,但写了个冗余的复合分支((p<r<q) || (q<r<p) 显式展开)。经典三分支写法:都小往左、都大往右、兜底即分岔 ——到达判断时 p、q 的值都 ≠ root.Val(指针相等被开头抓了,值互不相同保证值等⟺指针等),”都小”不成立且”都大”不成立,剩下的必然一左一右。四分支把逻辑写尽,三分支用排除法收拢 ——前提的边界都推过了,简化才安全。
迭代版藏了个更漂亮的小机锋:我给的骨架有 if root == p || root == q { return root }(命中判断),写的时候删了它——而这是对的:
1
2
3
4
5
p.Val == root.Val 时:
分支一:p.Val < root.Val → false
分支二:p.Val > root.Val → false
→ 掉进 else → return root ✓
等号是两个严格不等式的公共盲区,else 是盲区的收容所 ——“分岔”和”命中”(自己是祖先)不用区分,两种情况的答案都是 root。
4.4 支线翻车:树形二分的方向
先写了个 BST search 练手(“树形二分找节点”),实测四个全灭——方向反了 :
1
2
3
if ro.Val < target.Val { // 我比目标小
return search(ro.Left, ...) // ✗ 目标比我大,大的在右子树!
病根是主语搞反:记成了”小往左走”,但走哪边看的是目标在哪边 ——口诀钉死:“目标大往右,目标小往左,主语永远是目标” 。这和 LC11”谁大选谁”嘴瓢同族(那次的正确动作是”谁矮动谁”)——主语/宾语搞混 家族的又一案,且代码里一句对的话都看不出来,只有实测能钉死。
5. LC105:前序与中序构造二叉树(三版弧线)
preorder = [3,9,20,15,7], inorder = [9,3,15,20,7] → 重建原树。
5.1 为什么前序+中序能唯一确定一棵树
前序给”谁是根”,中序给”左右各分多少” ——信息互补:
1
2
3
4
5
6
7
8
9
10
preorder = [3, 9, 20, 15, 7] 中左右:3 是根,后面先是整个左子树、再是整个右子树
inorder = [9, 3, 15, 20, 7] 左中右:3 的左边全是左子树,右边全是右子树
↑
在中序里找到根 3,一刀切成两半:
左子树:前序 [9] 中序 [9] → 递归
右子树:前序 [20, 15, 7] 中序 [15, 20, 7] → 递归
↑
左子树 size = 1(从中序数出来),前序跳过 1 个就是右子树起点
顺带回答了经典追问:前序+后序为什么不行?两者都只能给根,没人给分割线 。
5.2 第一版:孤儿节点
第一版想用迭代重建(preIndex/inIndex 双指针逐个造节点),实测输出只有 root = 3——9、20、15、7 全部蒸发。死因是树系列最基础也最致命的一课:循环里 index = &TreeNode{...} 创建的新节点,没有任何一行代码把它挂到父节点上 。挂的只有新节点自己的孩子,但新节点自己是孤儿——root.Left 从头到尾没被赋值过。
树版的三步曲是”创建—挂载—递归 “——我做了创建,挂载整段蒸发。另外还有两个伴生病灶:prev 单变量想当栈用(只能回退一层,右套右就崩);inIndex < preIndex 跨数组比较(两个指针指着两个数组,大小关系没有语义)。
5.3 第二版:fail-silent——防御性代码静默吞掉了我的 bug
转递归分割,手推两层全对,代码四用例全灭——清一色空树 。主犯一行:
1
2
return build(0, len(preorder), 0, len(inorder)) // 闭区间右端应传 len-1
区间是闭的,右端传了 len——然后被我自己写的防御条件 preR >= len(preorder) 拦下,顶层直接返回 nil 。防御代码没报错、没 panic,安安静静把整个调用拦在门外。
这暴露了边界条件设计的真问题:六个条件的联合判断里,preL > preR 是真正的终止条件(闭区间空了),四个 >= len 是防越界的防御。防御性代码拦住 bug 时不该静默放行 ——正确的设计要么删掉防御(区间自洽,越界让它 panic 反而好查),要么防御触发时说明参数已经错了。Fail-silent 是最坏的失败方式:错误被吞,输出看起来合法,人眼看不出任何异常。
修好顶层后还有第二尸:查找循环 i < inR 漏查右端——左链用例 [1,2,3]/[3,2,1] 里 root=1 恰在中序最右(位置 = inR),循环永远找不到,index 停在初始值 inL,leftSize=0,整棵左链被判成右链,树整个镜像 。手推的两层里 root 都在中序中间,这个死角没被踩到——图纸对 ≠ 图纸全 :手推要用例覆盖边界形态(左链/右链),不只是”典型例子”。二分系列的开闭口诀(mid 判过可排除、l/r 没判过必须留住)在树上重演:闭区间的纪律——顶层len-1、循环 <=inR,错一个字符,树就空/镜像。
5.4 第三版:全绿
三处修复(len-1 / <=inR / preL++ 换成语义化的 leftStart := preL + 1),七用例全绿——左链死角、四层混合树全过。手推图纸里还揪出一处凑答案:右子树的中序起点我推成 preL+leftSize+1=2+1+-1=2(+-1 是凑出来的),正确公式是 index+1 ——中序里根的右边一格。中序的分割只看index 一个量,和 preL 无关。
6. LC124:最大路径和——双通道的满血(hard)
任意节点间路径的最大和,路径至少含一个节点。
树(一)543 立了双通道(返回值给父用、全局变量记答案),124 是它的满血考试——两味毒药:负节点 和“返回值和记录的不是同一个东西”的最经典案例。
6.1 第一版:六候选 max,一个返回值扛两种身份
1
2
3
4
return max(left, left+root.Val, root.Val, right, root.Val+right, left+root.Val+right)
↑
拐弯路径(两条腿)被 return 给了父
四用例三灭,两个死法都值钱:
死法一:分叉路径。 拐弯值(left+val+right)被返回给父,父再接上自己——路径分叉了。杀手用例:
1
2
3
4
5
6
0
/ \
1 2
/ \
3 4
节点 2 返回拐弯值 9(路径 3→2→4)→ 根算出 1 + 0 + 9 = 10——路径 1→0→3→2→4,在节点 2 处分叉 (进了 2 的左腿又出右腿还要再往上爬)。二叉树路径每边只走一次,10 是不存在的路径。数值上 10 > 合法最大 9,错误答案短得理直气壮。
死法二:nil 哨兵撞负值。 [-3] 输出 0——nil 返回 0,left<0 && right<0 不成立(0 不小于 0),max(0, -3, ...) = 0。这里 0 有两个身份:空链的值 (“不接这个孩子”= 贡献 0,合法)和 nil 哨兵 (子树不存在)——LC3 哨兵值域老坑换装重现(树(一)§2 的完整课)。
6.2 正解:双通道 + 接收端剪枝 + -MaxInt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
ans := -math.MaxInt
var gain func(r *TreeNode) int // gain = 以 r 为端点、向下的一条链的最大和
gain = func(r *TreeNode) int {
if r == nil {
return 0 // 哨兵:不存在,贡献 0
}
left := max(gain(r.Left), 0) // ★ 接收端剪枝:负链不接
right := max(gain(r.Right), 0)
ans = max(ans, r.Val+left+right) // 通道二:拐弯,只记录
return r.Val + max(left, right) // 通道一:一条腿,给父
}
gain(root)
return ans
四个要点,每个都对应第一版的一个死法:
- 通道分离 :拐弯(两条腿)只进
ans,绝不返回——一旦进了返回值通道,父节点无从知道它”已经用掉了两条腿”。通道二不是优化,是正确性的必需 (543 的双通道在 hard 的重考:那里的两条腿恰好不冲突,这里的分叉会直接造出非法路径) - 接收端剪枝 :
max(gain(child), 0)在接收端 ,不在递归出口——出口的 0 是 nil 哨兵,动了它负值答案全灭。剪枝只作用于”要不要带子链”,r.Val自己永远被算进去——所以”路径至少含一个节点”由根值保障,”负子树可以绕开”由剪枝保障,两个约束各管一段,不冲突 -MaxInt初始化:全负树[-2,-1,-3]的答案是 -1(单节点 -1 是最优路径),0 初始化会把它吃成 0- 负链不接的论证 :子树最优链是负的,接上只会拉低总和——而”至少一个节点”不在这里管(见 2)
6.3 教练翻车第三案:我错在考点上
终验用例里我设计了”负链剪枝”:10 / 左(-5,左3) / 右(-6),期望 8(路径 3→-5→10)。实测输出 10,我标 ✗——然后发现错的是我 :单节点 10 自己就是一条合法路径 ,不带任何孩子,路径和就是 10 > 8。正确答案不是”拐弯绕开负孩子”,是”干脆不带它们单飞”。
讽刺在:我错的知识点恰好是本题考点 ——”路径至少含一个节点”意味着单节点就是路径 ,这正是 ans 初始化 -MaxInt 的原因(全负树的最优就是单节点值)。出题时我自己都没把这条内化到位。教练翻车案累计三案(LC76 want 标错 B×2、LC543 数错边数、本案),规律一致:构造对了树,数错了答案 ——want 的计算比代码的编写更容易脱离检验,而出题人恰恰是最没有对拍保护的那个人。
7. 速查表
| 判据/口诀 | 一句话 |
|---|---|
| 动作位置定遍历 | 动作长在左递归前/中/后 = 前/中/后序——递归挪一行,迭代换骨架 |
| 迭代三兄弟 | 前序弹即处理+逆序压子;中序左链循环+两本账;后序窥视不弹出+lastVisited 记账 |
| 后序完成的铁证 | last == top.Right——右子树后序遍历最后处理的节点恰好是右子树的根 |
| BST = 立起来的二分 | mid 就是 root:数组二分压扁 BST,BST 搜索立起二分;不平衡 = 二分退化 |
| 性质换复杂度 | 多一个性质砍一层复杂度——单向门:通用解可降级使用,专用解撤性质即爆炸 |
| 方向判断口诀 | 目标大往右,目标小往左——主语永远是目标 |
| 前序+中序唯一确定 | 前序给根,中序给分割线;前序+后序不行:都只给根,没人给分割线 |
| 闭区间纪律 | 顶层 len-1、循环 <=inR——开闭错一个字符,树就空/镜像 |
| fail-silent | 防御代码拦住 bug 时静默返回合法值 = 最坏的失败方式;要么删防御,要么触发即报错 |
| 图纸对 ≠ 图纸全 | 手推要覆盖边界形态(左链/右链/单节点),死角在图纸里不存在,代码也想不到 |
| 双通道满血 | 拐弯只记录不返回(分叉非法);接收端剪枝(出口哨兵不动);-MaxInt(单节点也是路径) |
8. 树系列收官
三篇的地图:
1
2
3
4
树(一)递归心法:分解 vs 遍历、两通道(543 造形)、死亡信号(110)、哨兵信任(104)
树(二)BFS 与 LCA:队列+size 快照一族、短路/透传/合流(236)、返回值语义升维
树(三)BST 与收官:中序三用(98/230/235)、迭代三兄弟、构造(105)、双通道满血(124)
回头看本轮最值钱的三条主线:
- “中”的位置统治一切 ——动作位置定遍历(递归),时机决定骨架(迭代),分岔点定 LCA(BST),”中在最后”逼出记账(后序)。递归的三个遍历像一个人,迭代的三骨架像三个人——编译器替你管栈的时候,你感觉不到时机的重量 。
- 双通道从 543 到 124 的进化 ——543 是造形(两条通道恰好不冲突),124 是满血(不分会分叉)。通道分离不是代码风格,是正确性的必需——和树(二)236 的”模糊返回值+精确解读”合成返回值设计的完整光谱。
- 哨兵/边界的反复换装 ——LC3 的 0 撞合法下标、LC104 的设了不敢信、LC124 的 nil=0 撞负值、LC105 的 fail-silent、闭区间开闭。同一族坑在每篇换一件衣服登场,认出它是同一家子,比记住五个坑重要。
两篇预告的 BST 五题(98/230/235/701 插入/450 删除)实际完成三题,701/450 留作空位——不是遗漏,是诚实的账本:博客只写做过的题。下一站转向大厂高频线(146 LRU、215 TopK、25 K 组翻转……),树系列的装备(双通道、骨架复用、性质消费)在那里会继续点名。
参考资料
- 树(一):递归的艺术——分解与遍历、两通道与死亡信号
- 树(二):队列与祖先——BFS 骨架三配件、短路透传与合流
- 双指针与滑动窗口(四):二分——找点与找边界,两类模板与判定设计三课
- 配套代码仓库(按题号分目录):https://github.com/a18792721831/studyleetCode