这一篇在干嘛?

这一章把前面各章的”教学版”升级成”工业版”:STL map/set 真正用的红黑树、随机化平衡的 treap、字符串检索的后缀数组与后缀树、多维空间搜索的 kd 树,以及实践中最快、理论还没完全搞懂的配对堆。学完你就能看懂标准库和数据库索引的真身。

12.1 自顶向下伸展树

第 4 章的伸展树是”先递归找到、再一路旋回”。自顶向下版本在下降过程中就完成旋转(zig/zig-zag/zig-zig),用左树/中树/右树三棵子树的重新拼接实现,一次遍历即完成访问+伸展,不用父指针、不用栈:

图:自顶向下伸展的三种旋转

图:自顶向下访问 19 的拆解步骤

图:splaying 完成后的最终拼装

白话:把沿途经过的节点拆到”左边小树(都小于目标)“和”右边小树(都大于目标)“,最后三树拼接、目标登顶。摊还性质与自底向上版相同,但实现更快(一次下降)更省(无递归栈)。

12.2 红黑树:STL map 的真身

五条性质:① 每节点红或黑;② 根黑;③ 叶(NULL)黑;④ 红节点的孩子必黑(无红红相邻);⑤ 根到每片叶的路径黑高相同。可证:高 ≤ 2log(N+1)——平衡保证。

图:一棵红黑树(插入序列 1..)

自顶向下插入:一路”翻色”到底

下降途中遇到”双红孩子”就颜色翻转(父变红、子变黑),顺带修复可能的红红冲突;到达底部插入红节点。这样保证到达插入点时不需要回溯——旋转至多一次(局部),无递归回溯。

图:颜色翻转——只有 X 的父节点也是红时才需要继续处理

图:插入 45 后的调整

自顶向下删除:把红送到叶子

删除难点是”删黑节点会破坏黑高”。策略:让下降路径上始终保持”当前节点是红的”——把红情况化归为少数几种局部模式(旋转+翻色),到叶子时删除红节点即可,不破坏任何性质。

图:X 为左孩子且有两个黑孩子时的三种情形

白话:红黑树 vs AVL:AVL 更严格平衡(查得略快)、红黑树旋转更少(改得更快),且插入至多 2 次旋转、删除至多 3 次——写库(每次写入都要重平衡)选红黑树。与 AVL 的旋转同构:红黑树的翻色+旋转组合能一一对应到 AVL 的单/双旋转,学会一个另一个自然通。

12.3 Treap:树 + 堆的随机化婚姻

每个节点两个键:搜索键(满足 BST 序)+ 随机优先级(满足堆序:父 < 子)。优先级随机 ⇒ 树形状 = 随机 BST ⇒ 期望高 。插入 = 先按 BST 插入,再用旋转把新节点的优先级”顶”上去(最多 log N 次旋转)。

白话:把”平衡”外包给随机数——优先级是随机的,敌人构造不出坏输入(除非预知你的随机种子)。实现比 AVL/红黑树短一个量级,是算法竞赛里”要平衡树又懒得写红黑树”的标准答案。对偶理解:给每个元素一个随机权重后,treap 是唯一的 Cartesian 树——结构由数据完全确定,这就是它好写的原因。

12.4 后缀数组与后缀树:字符串检索的重炮

后缀:从每个位置开始的尾巴。如 banana 的后缀有 banana, anana, nana, ana, na, a

后缀数组 SA:把所有后缀排序后的下标数组。banana 的 SA = {5, 3, 1, 0, 4, 2}(a < ana < anana < banana < na < nana)。有了它:

  • 子串查询 = 在后缀数组上二分(子串必然是某后缀的前缀):
  • 相邻后缀的最长公共前缀(LCP)给出重复子串信息—— longest repeated substring、文档比较(diff)、生物序列分析的基础。

构造:朴素排序 ;倍增法 ;原书还讲线性 DC3。要点是”按前 k 个字符的排名做两次基数排序翻倍”。

后缀树:后缀的压缩 trie(公共前缀合并)。deed 的后缀 {d, eed, ed, deed} 的 trie:

图:deed 的后缀 trie

查询子串沿树下行 ——比后缀数组还快,代价是内存大(常数 10–20 倍)。Ukkonen 线性构造是字符串算法的巅峰之一。

白话:后缀 = 把字符串”所有可能的查找入口”预先排好队。DNA 测序、全文搜索引擎、代码查重,凡”在长文本里反复找模式”都靠这一族结构。

12.5 kd 树:多维空间里做二分

一维排序能二分查找,二维平面找”最近邻/范围查询”怎么办?2-d 树:偶数层按 x 分裂、奇数层按 y 分裂(kd 树推广到 k 维),建树

图:2-d 树样本

图:2-d 树对平面的划分——每个节点是一条切割线

图:四叉树对平面的另一种划分

白话:kd 树把平面切成嵌套的矩形格子,查询”半径 r 内的点”时只进与之相交的格子——绝大多数分支被整块剪掉。维度 ≤ 20 时实用;维度太高退化(维度灾难),高维近邻搜索转向 LSH/向量索引。

12.6 配对堆(Pairing Heap):实践最快的堆

结构:多叉树 + 左孩子右兄弟表示。所有操作都极简:insert/deleteMin/decreaseKey 的”整理”推迟到 deleteMin:取走根后,把孩子们的森林两两合并(第一趟)再从右往左收编(第二趟)

图:配对堆的抽象形态

图:左孩子右兄弟的实际表示

图:compareAndLink 合并两个子堆

白话:配对堆是”代码 100 行、实测最快、理论没证完”的传奇——decreaseKey 的摊还复杂度至今没有紧界(猜想 O(log log N))。Dijkstra/Prim 的大规模实现里它是二叉堆和斐波那契堆之外的第三极。

选型总结:有序字典 → 红黑树(通用库)/ treap(自写)/ 伸展树(热点访问);字符串检索 → 后缀数组(省内存)/ 后缀树(查询快);空间数据 → kd 树;可合并堆 → 配对堆(实践)/ 二项队列(理论齐整)。

常见坑

红黑树删除忘”保持当前红”的不变量:自顶向下删除若中途红变黑,叶子处的情形分析会爆炸——严格按书上的模式走。② treap 插入旋转方向写反:优先级小者在上(小根堆序),新节点向上旋转的方向取决于它和父节点谁的优先级小。③ 后缀数组二分比较:直接比较后缀是 每次比较,必须配合 LCP 信息或预处理,否则复杂度虚高。

通关标准

学完本篇你应该能做到:① 陈述红黑树五性质并解释为什么高 ≤ 2log N;② 描述自顶向下插入的翻色机制;③ 解释 treap 如何用随机优先级获得期望平衡、旋转如何维持双序;④ 手推 banana 的后缀数组并用它做子串二分;⑤ 说出配对堆 deleteMin 的两趟合并流程。