文章

【算法】树(三):BST 与中序——迭代三兄弟、立起来的二分,与双通道的满血收官

【算法】树(三):BST 与中序——迭代三兄弟、立起来的二分,与双通道的满血收官 摘要 树系列第三篇,也是收官篇。上半场 BST 子家族:LC98 凭记忆重写一次过(中序 + prev + 短路,验收带"父子全合法但整体不是 BST"的著名陷阱用例);LC230 第 K 小三版弧线——k-- 长错位置(动作位置课)→ 全量收集(被正名为对拍参照)→ 迭代版提前停(“栈在谁手里,停下的方式就是谁的风

【算法】树(三):BST 与中序——迭代三兄弟、立起来的二分,与双通道的满血收官

文章信息

  • 原文链接: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 永远不被处理)。三大死法:

  1. 处理完的节点没移走k-- 之后 cur 还指着它,下一轮压栈循环又把它压回去——重复处理,栈底的 1 永远轮不到弹出
  2. “中”节点永久丢失 :弹出即出栈,若它有右子树,压的是孩子——但自己已经不在栈里,右子树走完没人回来处理它
  3. 右链循环不回炉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 
每步看谁midroot
比一次,砍掉一半区间一棵子树
前提数组有序中序遍历有序 (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 停在初始值 inLleftSize=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

四个要点,每个都对应第一版的一个死法:

  1. 通道分离 :拐弯(两条腿)只进 ans,绝不返回——一旦进了返回值通道,父节点无从知道它”已经用掉了两条腿”。通道二不是优化,是正确性的必需 (543 的双通道在 hard 的重考:那里的两条腿恰好不冲突,这里的分叉会直接造出非法路径)
  2. 接收端剪枝max(gain(child), 0)接收端 ,不在递归出口——出口的 0 是 nil 哨兵,动了它负值答案全灭。剪枝只作用于”要不要带子链”,r.Val 自己永远被算进去——所以”路径至少含一个节点”由根值保障,”负子树可以绕开”由剪枝保障,两个约束各管一段,不冲突
  3. -MaxInt 初始化:全负树 [-2,-1,-3] 的答案是 -1(单节点 -1 是最优路径),0 初始化会把它吃成 0
  4. 负链不接的论证 :子树最优链是负的,接上只会拉低总和——而”至少一个节点”不在这里管(见 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)

回头看本轮最值钱的三条主线:

  1. “中”的位置统治一切 ——动作位置定遍历(递归),时机决定骨架(迭代),分岔点定 LCA(BST),”中在最后”逼出记账(后序)。递归的三个遍历像一个人,迭代的三骨架像三个人——编译器替你管栈的时候,你感觉不到时机的重量
  2. 双通道从 543 到 124 的进化 ——543 是造形(两条通道恰好不冲突),124 是满血(不分会分叉)。通道分离不是代码风格,是正确性的必需——和树(二)236 的”模糊返回值+精确解读”合成返回值设计的完整光谱。
  3. 哨兵/边界的反复换装 ——LC3 的 0 撞合法下标、LC104 的设了不敢信、LC124 的 nil=0 撞负值、LC105 的 fail-silent、闭区间开闭。同一族坑在每篇换一件衣服登场,认出它是同一家子,比记住五个坑重要。

两篇预告的 BST 五题(98/230/235/701 插入/450 删除)实际完成三题,701/450 留作空位——不是遗漏,是诚实的账本:博客只写做过的题。下一站转向大厂高频线(146 LRU、215 TopK、25 K 组翻转……),树系列的装备(双通道、骨架复用、性质消费)在那里会继续点名。

参考资料

本文由作者按照 CC BY 4.0 进行授权