树系列第 11 篇:红黑树——五条性质背后的直觉

欢迎来到“树系列”第 11 篇。第 8 篇我们看清了裸 BST 的软肋:插入顺序一旦和键的序关系“合谋”,树就会塌成一根链,查找从 O(log n) 崩坏成 O(n)。第 9 篇我们请出了第一位救火队员——AVL 树,它用“任意节点左右子树高度差不超过 1”的严格标准,把树高锁死在 1.44·log₂n 附近,代价是每次插入、删除后都可能要旋转,甚至一次删除要转 O(log n) 次。这一篇,第二位、也是工程上出场率最高的救火队员登场了:红黑树(Red-Black Tree)

红黑树大概是所有平衡树里“名声最大、劝退率也最高”的一位。打开任何一本算法教材,红黑树的章节都像一份晦涩的规章条例:节点非红即黑、根是黑的、NIL 叶子是黑的、红节点不能有红孩子、每条路径黑节点数相同——五条性质背下来不难,可很少有人能说清“为什么偏偏是这五条”“为什么是红色和黑色”“这些规则跟 O(log n) 有什么关系”。于是大多数学习者陷入两种困境:要么死记硬背五条规则和一堆修复案例,考试一过全部忘光;要么觉得红黑树是“另一个星球上的数据结构”,直接绕道而行。

本篇的态度很明确:红黑树不是背规则的树,而是一棵“会讲道理”的树。五条性质里没有一条是拍脑袋定的,每一条都在回答同一个问题——怎么用最小的维护代价,保证任意根到叶子的路径都不超过最短路径的两倍。我们将从“为什么 AVL 还不够”出发,先理解红黑树想解决什么问题;然后认识它的长相,理解“红色节点是借来的层”这个关键比喻;再逐条拆解五条性质,每条都讲清“为什么这么定”;接着引入黑高的概念,用它把“最长路径 ≤ 2 × 最短路径”和“n 个节点高度 ≤ 2·log₂(n+1)”两个结论讲出直觉;之后和 AVL 全面对比,看看工程界为什么把 Java TreeMap、C++ std::map、Linux 内核的默认选择都押给了红黑树;最后预览第 12 篇要详述的插入、删除修复,并澄清几个流传甚广的误解。

全篇配了十几张 SVG 示意图,从“第一棵红黑树长什么样”到“叔叔的颜色为什么是分水岭”,尽量做到每一个抽象概念都有一张对应的图。结尾依然有速查表、六道自测题(含答案)和下一篇预告。准备好了吗?我们先回到第 8、9 篇的终点,看看红黑树是从哪里接过接力棒的。

红黑树五条性质

0 先回顾:退化、AVL,以及“平衡”的代价

0.1 第 8、9 篇留下的问题

第 8 篇的核心结论可以浓缩成一句话:裸 BST 只保证内容正确,不保证形状健康,而性能恰恰由形状决定。一棵高度为 h 的 BST,查找、插入、删除都是 O(h);当树是“矮胖”的,h 只有 log₂n 量级,一切都很美好;当树被有序输入喂成一条链,h 逼近 n,一切都会崩坏。数据按主键批量导入、日志按时间戳追加、排行榜按分数录入,这些再常见不过的场景,恰好都是裸 BST 的“天然退化输入”。所以生产级系统不会接受裸 BST 的“平均快”,而是要求“最坏也有界”。

第 9 篇给出了第一个自平衡方案:AVL 树。它的规则极简——任意节点的左右子树高度差不超过 1。为了维护这条规则,AVL 在每次插入后沿祖先链检查平衡因子,发现失衡就做一次或两次旋转;删除时更麻烦,一次删除可能让多个祖先接连失衡,最坏需要 O(log n) 次旋转。AVL 的优点是树特别矮:n 个节点的 AVL 树,高度上限大约是 1.44·log₂(n+2),非常接近理论下界;查找性能因此在所有平衡树里名列前茅。

但第 9 篇结尾我们留了一个伏笔:AVL 的严格是有代价的。本篇第一件事,就是把这份代价摊开来看,并论证红黑树为何选择了另一条路线——近似平衡

0.2 本篇路线图

本篇不是红黑树的“操作手册”,而是“设计说明书”。第 12 篇才会手把手实现插入和删除的修复流程,本篇只做三件事:讲清楚红黑树为什么长这样、为什么这五条性质能推出 O(log n)、以及它和 AVL 到底谁更值得用。具体来说:第 1 节分析 AVL 的旋转负担,引出“近似平衡”的动机;第 2 节认识红黑树的长相,建立“红色节点是借来的层”的直觉;第 3 节逐条拆解五条性质;第 4 节定义黑高并动手计算;第 5 节用黑高推出最长路径和高度上限的直觉证明;第 6 节和 AVL 全面对比;第 7 节预览插入删除的修复思路;第 8 节澄清常见误解;第 9 节动手体验;最后速查表、自测题、下一篇预告。

1 为什么不用 AVL 就够了吗

1.1 AVL 的“较真”哲学

先给 AVL 一个公允的评价:它不是不好,而是太较真。AVL 对“平衡”的定义是全宇宙最严格的一档——任意节点的左右子树高度差不能超过 1。这意味着 AVL 树在形态上已经逼近“完美平衡”的天花板:几乎所有节点都处在一个“左右几乎等高”的状态,树高被压到理论下界附近。

“严格”本身不是缺点,读多写少的场景里,AVL 的矮树身恰恰是巨大优势。但严格是有代价的,这个代价藏在每一次写入里。我们来算一笔账:插入一个新节点,它只能挂在某个叶子下面,于是从它到根的所有祖先,高度都有可能增加 1。AVL 规定高度差不能超过 1,所以任何一个本来“差 1 才失衡”的节点,都可能因为这一格高度的增加而变成“差 2”,触发旋转。换句话说,AVL 把每一次插入都当成一次潜在的“危机”来对待——虽然一次插入最多只需要旋转一处,但“检查”这件事本身要从新节点一路走到根,而且在最坏情况下,旋转的确会发生。

删除更不客气。AVL 删除一个节点会让某棵子树变矮,而变矮可能让祖先“从平衡变成不平衡”,旋转后子树高度恢复、又可能让更上面的祖先重新失衡……于是一次删除最坏需要 O(log n) 次旋转,第 10 篇我们详细分析过这个“失衡向上传染”的过程。用大白话说:AVL 对每次写入都像强迫症一样要求“恢复原状”,于是写入路径上每一步都要严阵以待

1.2 代价到底有多大:旋转是昂贵的手术

为什么“旋转次数多”值得专门拿出来说?因为旋转不是免费的。一次旋转要改写若干个指针:把新根提上来、把旧根放下去、把中间子树过继给另一边。在 C++ 或 Java 的实现里,每个节点还要维护父指针,旋转时要同步更新 parent 关系,代码复杂度和出错概率都随之上升。更关键的是,旋转会改变子树的根,如果这棵子树不是整棵树的根,还要把它“重新挂回”原来的父节点——这个“回挂”操作恰恰是各种教科书代码里最容易写错的地方。

但旋转最大的代价其实不是常数时间的指针操作,而是局部性:旋转会让树的形状发生结构性改变。AVL 树里一次删除最多可能触发 O(log n) 次旋转,这意味着删除一次键,树的形状可能发生“连锁反应”式的变动。虽然每次旋转都是 O(1),常数次旋转也依然是 O(1),但在高频写入的系统里,这些额外的指针操作和分支判断累积起来,就变成了肉眼可见的吞吐差距。

再补一个容易被忽略的细节:AVL 为了判断“要不要旋转”,每个节点都要存储高度或平衡因子,并且插入、删除后要从改动点一路向上重新计算。高度计算涉及比较左右孩子的高度,每层都要做。这些操作同样都是 O(1) 常数,但常数和常数之间也有大小之分——AVL 的“维护税”是按每层、每次都缴的。

1.3 换个思路:允许“局部不完美”

如果跳出“每次都要把树修到完美”的执念,会看到一个截然不同的设计空间:能不能允许树局部不那么平衡,只要整体高度仍然 O(log n) 就行? 换句话说,不要求“左右子树高度差 ≤ 1”,而是要求一个更宽松、但依然能推出 O(log n) 的约束。

这个思路的妙处在于:约束越宽松,触发“修理”的时机就越少,修理的成本就越低。就像交警查酒驾:如果规定“滴酒不沾”,几乎每次聚餐都要查一轮;如果规定“不超过某个安全阈值”,大部分聚餐根本不用管,只有少数超标才需要拦下来。红黑树选择的,正是后一种策略——它允许树“歪”一点,但保证任何一条根到叶子的路径,都不会超过最短路径的两倍

注意这个目标的措辞:“任意两条根到叶子的路径,长度之比不超过 2”。这比“左右子树高度差不超过 1”宽松得多:高度差不超过 1 的树,任意两条路径长度差也必然不超过 1(事实上最多差 1),而红黑树允许路径长度差接近一倍。所以红黑树的最坏高度大约是 2·log₂(n+1),是 AVL(约 1.44·log₂n)的约 1.4 倍。代价是树变高了一点,收益是维护时少做很多事

1.4 红黑树的设计目标

把上面的讨论收拢,红黑树的设计目标可以写成三句话:

  1. 最坏情况有界:任何操作都在 O(log n) 内完成,绝不接受 O(n) 的退化;
  2. 维护成本可控:插入、删除后的修复操作,平均和摊还次数都远少于 AVL;
  3. 规则可证明:五条颜色规则要能推出“最长路径 ≤ 2 × 最短路径”,从而推出 O(log n)。

这三句话里,第二句是红黑树存在的真正理由。如果只追求第一句,AVL 已经做得很好了;如果只追求第三句,随便设计一组规则都行。红黑树的独特之处在于,它用“颜色”这个轻量级标记,把平衡维护的代价降到了最低:大部分修复只需要改颜色,只有少数情况需要旋转。变色是 O(1) 的、连指针都不动;旋转虽然贵一点,但红黑树的旋转发生频率远低于 AVL。

我们还可以从另一个角度看红黑树的“务实”:它是为了写入场景设计的。如果世界是只读的,AVL 是最优解;但现实世界的动态集合读多写少、写多读少、读写均衡全都存在,标准库不能假设负载形态,于是它选择了“平衡程度略差、但写入便宜”的红黑树。这个选择不是红黑树“比 AVL 高级”,而是工程权衡的必然结果——第 6 节我们会用数字和场景把这件事讲透。

在进入红黑树的长相之前,先记住一句话:红黑树的一切设计,都可以追溯到“近似平衡 + 低维护成本”这对目标。五条性质不是一堆孤立的禁令,而是实现这两个目标的配套机制。接下来,我们就来看看这棵“会讲道理”的树到底长什么样。

2 红黑树长什么样

2.1 第一棵红黑树:每个节点多一个颜色

红黑树首先是一棵二叉搜索树:中序遍历严格递增、查找规则和普通 BST 完全一样。它和普通 BST 的唯一区别是:每个节点多了一个属性——颜色(color),取值只有红色或黑色。先看一棵具体的红黑树,存的是 1 到 9 这九个键:

第一棵红黑树:键 1–9,空位全部画成黑色 NIL 5 3 8 1 4 7 9 NIL NIL NIL NIL NIL NIL NIL NIL

图 1:一棵合法的红黑树——黑色节点构成骨架,红色节点点缀其上,每个空子树都补上黑色 NIL 作为统一终点。

等等,这张图里怎么多了这么多“NIL 黑”节点?这正是红黑树和普通 BST 的一个关键差异:红黑树把所有空子树显式地画成一个特殊的叶子节点,叫 NIL(或哨兵),并且规定 NIL 是黑色的。普通 BST 里“没有孩子”是一个空洞,红黑树里“没有孩子”也是一个实实在在的节点。这样做的目的我们马上会讲到,这里先把它当成一个约定:树的每个内部节点都有两个“孩子”,如果原本没有孩子,就用一个黑色 NIL 补上。

先检查这棵树是不是合法的红黑树。根 5 是黑色,合格;每个 NIL 是黑色,合格;红色节点 3 的孩子是 1(黑)和 4(黑),红色节点 8 的孩子是 7(黑)和 9(黑),没有出现“红-红”相连,合格;再看“从任一节点到后代 NIL 的路径上黑节点数相同”这条——以根 5 为例,到任何一个 NIL 的路径上,黑节点包括 5、NIL 本身,以及路径上所有黑色内部节点。左子树里从 5 到 1 的左 NIL:5(黑)、3(红)、1(黑)、NIL(黑),共 3 个黑节点;从 5 到 1 的右 NIL:5、3、1、NIL,也是 3 个;从 5 到 7 的左 NIL:5(黑)、8(红)、7(黑)、NIL(黑),还是 3 个。所有路径一致,第五条性质也满足。这是一棵合格的红黑树。

细心的读者可能已经发现:这棵树的“黑色骨架”恰好是一棵满的二叉搜索树——5、1、4、7、9 都是黑色,只有 3 和 8 是红色。这不是巧合,而是红黑树的常态:黑色节点构成了树的“骨架”,红色节点像装修一样点缀在骨架上。沿着这个思路,我们进入本节最重要的一个比喻。

2.2 红色节点是“借来的层”

“红色节点到底是什么?”这是学习红黑树时最先冒出来的问题。教科书只会说“红色是规则的一部分”,但直觉上的解释是:红色节点是“借来的层”

想象一棵所有节点都是黑色的树,它像一栋严格的公寓楼:每一层都住满了“正式住户”,从楼顶到地面,每层之间高度差整齐划一。这栋楼的问题是太死板——它本质上是一棵满二叉树,任何插入都可能瞬间破坏“每层全满”的完美结构,然后引发大规模重新装修。AVL 就是那个把“楼层高度差 ≤ 1”写进物业规定的严格管理员。

红黑树的管理员聪明得多:它允许某些节点“暂住”在别人的楼层里——这些暂住户就是红色节点。红色节点不占“正式楼层”的额度:在计算一条路径的“黑高”时,红色节点根本不被计数,只有黑色节点才算数。于是树可以在不增加黑色层数的情况下,临时多塞进一些节点,就像在两层楼之间搭一个夹层。夹层的好处是显而易见的:树可以长得比纯黑色骨架更“丰满”,容纳更多节点,却不用改变骨架的层数。

但夹层不能无限搭,否则楼就塌了。物业规定:红色节点的孩子必须是黑色——也就是说,夹层下面不能马上再叠一个夹层。两个红色节点不能父子相连,这保证了任何一条路径上,红色节点不会“连续挤在”一起,路径中红、黑两色必然交替出现。把这句话和“每条路径黑节点数相同”合起来,就得到一条惊人的推论:任意一条根到叶子的路径上,红色节点数不可能超过黑色节点数,所以最长路径至多是最短路径的两倍。这就是第 5 节会详细展开的定理,现在先让比喻落地:

  • 黑色节点 = 正式楼层,决定树的“骨架高度”;
  • 红色节点 = 夹层,用来装更多节点而不增加骨架高度;
  • “红节点孩子必须是黑” = 夹层不能叠夹层;
  • “每条路径黑节点数相同” = 每部楼梯经过的正式楼层数必须一致。
红色节点 = 借来的层:夹层不增加黑高 楼层比喻 黑:正式楼层 3 (计入黑高) 红:夹层(不计数) 黑高不变 黑:正式楼层 2 (计入黑高) 红:夹层(不计数) 黑高不变 黑:正式楼层 1 (计入黑高) 黑:NIL 地面 所有路径的公共终点 对应的红黑树路径 5 3 1 NIL 黑 黑节点数:3 个(5、1、NIL) 红节点 3 不占层数

图 2:红色节点像两层正式楼层之间的夹层——不计入黑高,所以路径 5→3→1→NIL 的黑节点只有 5、1、NIL 三个。

有了“借来的层”这个比喻,红黑树的很多规则就不那么像天书了。比如为什么根必须是黑色?因为根是整栋楼的最高层,它要是红色(夹层),楼顶就悬空了;而且把根变黑往往还能顺手满足其他性质,成本几乎为零。为什么 NIL 叶子是黑色?因为 NIL 是“地面”,地面当然要计入正式楼层,否则“每条路径黑节点数相同”就失去了统一的计数基准。这些“为什么”我们在第 3 节逐一展开。

2.3 NIL 叶子:把“空”变成节点

在继续之前,必须把 NIL 这个概念钉死,否则后面读性质三、性质五时很容易犯晕。在大多数教材里,红黑树有两种画法:

画法一(省略版):只画内部节点,不画 NIL,看起来和普通 BST 一样。优点是图干净;缺点是在验证性质五(黑高相同)时,容易漏算“每条路径终点上的那个黑色 NIL”。

画法二(显式版):把所有空子树画成黑色 NIL 节点,如图 1 那样。优点是定义严谨——每条路径都有明确的终点,性质五的计数有统一基准;缺点是图变得很“挤”。

本篇两种画法都会用:示意图优先用省略版,讲性质时用显式版。无论是哪种画法,都要记住 NIL 的两个事实:第一,NIL 不是真正的“数据节点”,它不存储键,只是占位符;第二,NIL 是黑色的,而且通常实现为所有 NIL 共享同一个哨兵对象(在 C 语言里可以是一个全局静态变量,在 Java 里可以是一个静态 final 字段),这样每次判空都省一次内存分配。

同一棵树的两种画法:省略 NIL 与显式 NIL 省略版:不画 NIL 5 3 8 1 4 7 9 图干净,但验证黑高时容易漏算 NIL 显式版:空子树画成黑色 NIL 5 3 8 1 4 7 9 NIL NIL NIL NIL NIL NIL NIL NIL

图 3:省略版与显式版画的是同一棵树——显式版把每个空位补成黑色 NIL,性质五的计数才有统一终点。

顺带一提,NIL 的存在也解释了为什么红黑树的性质五要说“到后代叶子的路径”,而不是“到根”。叶子(NIL)是所有路径的共同终点,统一了计数基准;如果没有这个终点,一条“在中间就断了”的路径和一条“走到最深处”的路径根本无法比较。这是红黑树定义里最容易忽略、却最体现设计用心的一处细节。

2.4 一个容易被忽略的事实:查找根本不用看颜色

红黑树的一切“高级操作”都发生在插入和删除之后,而查找完全不用看颜色。从根出发,把目标键和当前节点比较,小则向左、大则向右、相等则命中——和普通 BST 一字不差。为什么颜色不影响查找?因为颜色是“平衡元数据”,只用于维护树形,不参与键的比较和路径选择;只要树形合法,BST 的“左小右大”性质就保证查找正确。这意味着所有 BST 的扩展能力——中序遍历有序、前驱后继、区间查询、第 k 小——都可以原封不动地搬到红黑树上,平衡机制被完全封装在插入、删除两个操作内部。

工程上这个事实还有一层意义:红黑树可以做成“即插即用”的替换品。如果你有一段代码用的是普通 BST,把实现换成红黑树,对外接口和行为(查找、有序遍历)完全不变,只是最坏性能从 O(n) 变成 O(log n)。Java 的 TreeMap、C++ 的 std::map 之所以敢公开承诺“所有操作 O(log n)”,正是因为有这层“查找不看颜色、平衡不外泄”的设计隔离。

3 五条性质逐条拆解:每条规则为什么这么定

红黑树的五条性质是各种教材的标准配置,我们逐条来。每一条都先说“是什么”,再说“为什么这么定”,最后用一小段“如果去掉会怎样”来检验我们的理解。

3.1 性质一:节点非红即黑

规则:树中的每个节点要么是红色,要么是黑色,没有第三种颜色。

这条看起来像废话,但它其实是整个机制的地基。颜色是红黑树唯一的“状态变量”,所有平衡规则都建立在这枚二值标记上。为什么是两种颜色而不是三种、四种?因为红黑树的全部目的只是区分“正式楼层(黑)”和“借来的夹层(红)”这两类节点,两种状态就够用了。颜色越多,规则越复杂,收益却不会增加——平衡性由“红黑交替 + 黑高一致”决定,与颜色的具体种类无关。为什么偏偏叫红和黑?纯属历史习惯:发明者 Robert Sedgewick 在他的博士论文中使用红、黑两种墨水分层绘制 2-3 树,红色连接代表“融合进上一层的节点”,这个命名就流传了下来。可以说,红黑树的名字本身就暗示了它与 2-3-4 树的血缘关系(第 8 节预告)。

工程上,“非红即黑”意味着每个节点只需一个布尔位(或一个枚举)来存颜色。与 AVL 必须存储高度或平衡因子(一个整数)相比,颜色在空间上更省——很多实现甚至用指针最低位来编码颜色,做到“零额外空间”。当然,现代实现很少抠这一位,但这个事实说明红黑树的“轻量”是从头到尾贯彻的。

如果去掉这条性质会怎样?如果允许第三种颜色,比如“蓝色节点”,那“红黑交替”“黑高一致”等规则就必须全部推广到三色,规则复杂度会爆炸,而平衡性不会变得更好——因为两种颜色已经足够编码“正式”和“暂住”两种状态。所以性质一是“最便宜的规则”:它用最小状态空间,支撑起了后面四条性质的全部表达力。

3.2 性质二:根是黑色的

规则:整棵树的根节点必须是黑色。

为什么根必须是黑色?有三个层次的答案。第一层,美学与简洁:如果根是红色,它的“上层”什么都没有,红色节点“借来的层”就失去了意义——夹层总得夹在两层正式楼层之间,楼顶不能是悬空的夹层。第二层,规则的连带效应:红黑树的插入修复有一个非常常见的收尾动作——把新插入的红色节点一路“往上染”,如果某一步不小心把根染成了红色,性质四(红节点不能有红孩子)可能仍然满足,但根是红色会让我们失去“黑色骨架”的参照系;规定根为黑,相当于强制树的最高点是正式楼层,所有路径的黑色计数都从同一个黑色起点出发。第三层,代价为零:把根变黑永远不会破坏任何其他性质。根变黑只会增加所有路径上的黑节点数(每一条路径都经过根),而性质五要求的是“所有路径黑节点数相同”,大家同时 +1,依然相同;性质四要求红节点的孩子是黑,根变黑只会减少红色节点,更不会违规。所以“根为黑”是一条几乎免费的保险。

实际上,算法实现里插入修复的最后一步通常就是“把根涂黑”(root 的颜色强制设为 black),这一行代码既兜底了所有边界情况,也让“根是黑”永远成立。很多教材说这是“性质二的强制恢复”,准确地说,它是实现层面对性质二的主动维护。

如果去掉性质二会怎样?树依然可能是“部分合法”的,但有两个麻烦:一是根若为红,根的父节点不存在,性质四对根自动放宽,规则就有了“特殊例外”,定义不再统一;二是黑色骨架的计数基准会随根的颜色漂移,不利于证明和推理。为了省一条规则而引入两个例外,显然不划算。所以性质二不是锦上添花,而是“让规则没有例外”的必要设计。

3.3 性质三:叶子(NIL)是黑色的

规则:每个叶子节点(即 NIL,空子树哨兵)都是黑色。

这一条是初学者最容易忽略、也最容易问“凭什么”的。先澄清:这里的“叶子”不是普通 BST 里的“没有孩子的数据节点”,而是显式的 NIL 占位节点。性质三说的就是 2.3 节那些哨兵必须是黑色的。

为什么?因为 NIL 是所有路径的公共终点,是“地面”。性质五要求“从任一节点到后代叶子的路径上黑节点数相同”,如果 NIL 不参与计数,或者 NIL 的颜色不统一,那么“到叶子”这个终点就失去了基准:有的路径停在“浅处的空位”,有的路径停在“深处的空位”,两者黑节点数不可比。把 NIL 定为黑色,就相当于规定每部楼梯的底层地面都是正式楼层,计数从地面开始统一。

还有一种更功利的解释:如果 NIL 是红色,那么任何红色节点都可以有一个“红色孩子 NIL”,性质四“红节点的孩子都是黑”就永远无法对叶子成立,红黑交替的保证被撕开一个口子——只要走到 NIL 就合法地出现“红-红”,那“最长路径 ≤ 2 × 最短路径”的证明就崩了。反过来,NIL 为黑让“红节点的孩子都是黑”对每一个孩子(包括空孩子)都成立,规则没有例外。

工程上的细节:所有 NIL 共享一个哨兵对象,它既当左空、右空,也当整棵树的“空根”。这样查找、插入代码里的判空逻辑全部统一为“节点 == 哨兵”,不必区分“孩子为空”和“节点为叶子”两种情形。红黑树实现之所以能写得又短又稳,很大程度上归功于这个统一哨兵。

如果去掉性质三会怎样?性质五的计数基准立刻乱掉:同样一条物理路径,到“左边空位”和“右边空位”的黑节点数可以不同,红黑交替也会在路径末端失真。可以说,性质三是性质四和性质五的共同地基,没有它,后两条性质根本无法自洽。

3.4 性质四:红节点的孩子都是黑色的(不允许连续红)

规则:如果一个节点是红色的,那么它的两个子节点都必须为黑色;等价地说,红节点的父节点也必须是黑色(如果存在)。任何一条路径上不允许出现两个连续的红色节点

这是五条性质里“行为最具体”的一条,也是插入修复时最常被触发的一条。为什么必须禁止连续红?回到“借来的层”比喻:红色节点是夹层,夹层下面不能再叠夹层,否则楼的结构就不稳定。数学上的原因是:连续红会让“红色节点数”在路径上失去上限,破坏“最长路径 ≤ 2 × 最短路径”的证明。

我们来具体推导一下。性质五保证每条路径上的黑节点数相同,设为 bh(黑高)。在性质四的约束下,任意路径上红色节点不能相邻,因此路径上的红色节点数至多等于黑色节点数(更准确地说,红节点要么出现在两个黑节点之间,要么出现在起点和第一个黑节点之间,无论哪种,数量都不超过黑节点数)。所以任意路径总长度 = 黑节点数 + 红节点数 ≤ bh + bh = 2·bh。而最短路径——我们马上会看到——恰好是“全是黑节点”的路径,长度就是 bh(含 NIL 的计数约定另说)。于是最长路径 ≤ 2·bh = 2 × 最短路径。性质四和性质五联手,才锁死了路径比

如果允许连续红,会怎样?假设一条路径上红节点可以无限堆积,比如“黑-红-红-红-黑”,那么这条路径的总长度可以远超最短路径的两倍,树的形状就失去了约束。历史上确实有研究者尝试过“放宽连续红”的变体(比如允许连续两个红节点的 AA 树),但代价是引入更复杂的分类;经典红黑树选择“完全禁止连续红”,换来的是修复规则的高度规整。

还要注意性质四和性质五的一个微妙互动:性质五约束“黑节点数相同”,性质四约束“红节点不能扎堆”。黑高一致保证路径长度不会因为“黑的多寡”而失衡,红黑交替保证路径长度不会因为“红的扎堆”而失衡,两条规则一横一纵,把路径长度牢牢夹在 bh 和 2·bh 之间。这个“一横一纵”的画面,是理解红黑树最重要的心智模型。

性质四:红节点的孩子必须是黑 合法:红黑交替 5 3 8 1 4 7 9 每个红节点下面都是黑节点,红黑交替 违规:出现连续红 5 3 8 2 连续红! 4 7 9 红色 3 的左边竟还是红色 2,红-红相连

图 4:性质四只做一件事——禁止红-红相连;右侧把 3 的左孩子换成红色 2 后,红黑交替立即被撕开。

3.5 性质五:从任一节点到后代叶子的路径上,黑节点数相同(黑高)

规则:对树中的任意节点 x,从 x 出发到它所有后代叶子(NIL)的每一条简单路径上,黑色节点的数量都相同。

这是五条性质里分量最重的一条,红黑树所有性能保证都建立在它之上。它直接给出“黑高”(black-height)这个核心概念:从节点 x 到叶子路径上的黑节点数(不含 x 本身,含 NIL,具体约定各家略有出入,第 4 节统一口径),记作 bh(x)。性质五说的就是:x 的所有后代路径,黑高都等于 bh(x)

为什么必须“每条路径黑节点数相同”?因为只有黑节点才真正“承担”树的骨架高度。如果不同路径的黑节点数不同,那路径长度就跟着失去控制——有的路径只有 1 个黑节点、却挂着几十个红节点,有的路径有 10 个黑节点,树的形状会变成“长短腿”,退化的幽灵又会回来。性质五把“黑骨架”的高度锁死,性质四再限制“红夹层”的厚度,两条合起来,树的高度就有了硬上限。

再往深处想一步:性质五其实在暗示一个更本质的事实——红黑树的黑色节点构成了一棵“黑高一致的树”。如果把所有红色节点“折叠”进它们的黑色父节点(这正是 2-3-4 树的视角),黑色骨架就会变成一棵完美平衡的多路树:每条根到叶子的路径层数完全一致。红黑树的“平衡性”并不神秘,它本质上是把一棵“完美平衡的 2-3-4 树”用二叉树重新编码出来的结果。这个洞察我们留到第 8.3 节展开,现在只要记住:性质五 = 黑色骨架完美平衡

如果去掉性质五会怎样?只保留前四条性质的树可以非常畸形:一条全是红色的“红黑交替链”(黑-红-黑-红-黑-红……)完全满足前四条——没有连续红、根是黑、NIL 是黑——但它的高度可以是 O(n),查找照样退化。所以前四条性质负责“整齐”,性质五负责“平衡”;没有性质五,红黑树就只是“涂了颜色的 BST”,没有任何性能承诺。这一点极其重要,值得反复咀嚼。

3.6 五条性质合起来:一张图读懂

把五条性质标注在同一棵树上,你会看到它们各自管什么、彼此如何衔接:

五条性质各管一处:同一棵树上把它们全部标出来 5 根:黑色(性质二) 3 8 1 4 7 9 NIL 黑 (性质三) NIL 黑 NIL 黑 NIL 黑 NIL 黑 NIL 黑 NIL 黑 NIL 黑 性质四 红节点的孩子都是黑 性质四 红节点的孩子都是黑 性质五 每条路径黑节点数相同

图 5:五条性质的分工一目了然——性质一给颜色、性质二管树顶、性质三管树底、性质四管红、性质五管黑。

这张图里:性质一给每个节点贴了颜色;性质二约束树顶;性质三约束树底;性质四约束“红与红”不能相邻;性质五约束“黑与黑”的分布必须均匀。五条性质各管一个维度,不多不少。

3.7 五条性质的最小性:去掉任何一条会怎样

理解了每条性质的“为什么”之后,一个自然的追问是:这五条是不是缺一不可?答案:性质四和性质五缺一不可,性质一、二、三更多是“让规则自洽且无例外”的必要支撑。用一张表把“去掉某条性质”的后果列出来,比任何抽象论证都直观:

去掉的性质树会变成什么最坏树高
性质一(非红即黑)规则失去表达力,颜色不再是二值状态规则体系崩塌
性质二(根为黑)根可为红,性质四在根部出现“无父可查”的例外仍可能是 O(log n),但定义不自洽
性质三(NIL 为黑)黑高计数失去统一终点,性质四对空孩子失效高度上界失去证明
性质四(无连续红)红色节点可以无限扎堆,路径长度失去上限可退化为 O(n)
性质五(黑高一致)黑色骨架可长可短,红黑交替救不了“长短腿”可退化为 O(n)

特别值得玩味的是“性质四、五单独保留”的情形:只留性质五、去掉性质四,树依然可以是“黑高一致但红节点叠罗汉”的畸形结构,红节点数量没有上限,路径长度照样失控;只留性质四、去掉性质五,树可以是“红黑交替但黑高各不相同”的歪树,最短路径和最长路径的黑节点数不同,二倍关系同样不成立。只有四、五联手,才同时锁住“红的厚度”和“黑的高度”

还有一个常被问到的点:为什么不允许“连续两个红”,而不是“连续三个”?答案是工程取舍。允许连续两个红(即红色孩子可以有一个红色孩子)确实也能推出 O(log n) 的高度上界,对应的变体就是 AA 树(用“只有右孩子可以为红”的附加规则简化删除)和 2-3 树编码;但允许的连续红越多,修复分支越多,实现越复杂。经典红黑树选择“一个都不允许”,换来的是插入、删除修复都只有有限几种标准情形——规则越干净,代码越可靠,这是红黑树设计者最看重的工程品质。

4 黑高:红黑树的“水平仪”

4.1 bh(x) 的定义

五条性质里的第五条约定了“黑节点数相同”,但到底数哪几个节点、从哪里数到哪里,需要统一口径。本书采用算法教材最常用的定义:

bh(x) = 从节点 x 出发(不含 x 本身),到任意后代叶子(NIL)的
        简单路径上的黑色节点个数

三个容易混淆的点先说清楚:

  1. 不含 x 本身:bh(x) 数的是 x“下面”的黑节点,x 自己的颜色另算。这样定义的好处是,计算 bh(红节点) 和 bh(黑节点) 时不用回头改父节点,且“父节点颜色”与“子树黑高”解耦。有些教材把 x 自己算进去,会得到“孩子黑高 = 父亲黑高 − 1(当父亲为黑时)”的变体,两种口径本质等价,但全篇必须统一。
  2. 含 NIL:每条路径的终点 NIL 是黑色,所以 bh(x) 至少是 1(只要 x 有孩子,最浅的路径终点就是 NIL)。这也解释了为什么说“空树的黑高是 0”:空树连 NIL 都没有,我们约定 bh(NIL) = 0,让定义在边界上自洽。
  3. 所有后代路径一致:性质五保证 x 的所有后代路径黑高相同,所以 bh(x) 是一个良定义的数,而不是“取最大值”或“取最小值”。这正是黑高能成为“水平仪”的前提。

有了黑高,红黑树的两条核心不等式就能写成干净的形式。设 x 是任意内部节点:

  • 最短的根到叶子路径长度(按节点数计)≈ bh(x) + 1(全是黑节点 + NIL 终点);
  • 最长的根到叶子路径长度 ≤ 2·(bh(x) + 1)(黑红交替,红节点至多与黑节点一样多)。

第 5 节会把这些不等式变成严格的直觉证明。现在先练一练怎么手算黑高。

4.2 计算例子:给每个节点贴一张“黑高牌”

还是用第 2 节那棵树,给每个内部节点标上 bh 值。为了便于对照,我们先把树重新画出来,并把每个节点写成“键(颜色,bh)”:

黑高牌:红色节点不计数(初版标注) 5 bh=2 3 bh=1 8 bh=1 1 bh=1 4 bh=1 7 bh=1 9 bh=1 NIL bh=0 NIL bh=0 NIL bh=0 NIL bh=0 NIL bh=0 NIL bh=0 NIL bh=0 NIL bh=0 初版按“红色不计数”标注 bh(3)=bh(8)=1,下文递推会修正为 2

图 6:黑高牌从叶子往上贴——NIL 为 0、黑叶子为 1、红色节点暂按“不计数”处理,这正是下文要修正的口径。

从叶子往根算。NIL 的 bh 是 0。节点 1 是黑色叶子(两个孩子都是 NIL):bh(1) = 1,因为从 1 出发到 NIL 的路径上,黑节点只有 NIL 一个。节点 3 是红色,孩子是 1(黑)和 4(黑):bh(3) = 1,因为到任一条路径上,黑色节点只有孩子 1(或 4)和 NIL,共 1 个(3 自己是红的,不计数)。节点 5 是黑色,左子树根是 3(红)、右子树根是 8(红),两条路径上的黑节点数都是 2(比如左路:3 下面的 1、NIL;右路:8 下面的 7、NIL),所以 bh(5) = 2。

验证一下通用递推公式:对于任意节点 x,bh(x) = 黑孩子的 bh + 1,红孩子的 bh 不变(若孩子是 NIL 则 bh = 0)。用 x = 5 验算:左孩子 3 是红,bh(3) = 1;右孩子 8 是红,bh(8) = 1;x 自己是黑,所以 bh(5) = bh(黑孩子) + 1 = 1 + 1 = 2。用 x = 3 验算:孩子 1、4 都是黑,bh(1) = bh(4) = 1,bh(3) = 1 + 1 = 2?等等,这里要小心:bh(x) 数的是 x 之下的黑节点,当 x 是红节点时,孩子的黑高不需要“加自己”。我们重新推一遍递推公式。

设 y 是 x 的孩子。如果 y 是黑色,那么从 x 出发到叶子经过的黑节点数 = 从 y 出发经过的黑节点数 + 1(多算了 y 自己);如果 y 是红色,那么从 x 出发到叶子经过的黑节点数 = 从 y 出发经过的黑节点数(y 不计数,路径上的黑节点和从 y 数起的完全相同)。于是:

bh(x) = bh(黑孩子) + 1     (若黑孩子存在)
bh(x) = bh(红孩子)         (若 x 只有红孩子,且红孩子的黑高相同)

性质五保证两个孩子的 bh 相同,所以不管 x 有左孩子还是右孩子,公式都成立。用 x = 3(红)验算:孩子 1(黑)的 bh = 1,bh(3) = 1 + 1 = 2?不对——bh(3) 刚才手算的是 1。哪里出错了?问题出在“含 NIL”的约定:bh(1) = 1 指的是“从 1 出发到 NIL 路径上的黑节点数”,这条路径包括 NIL 和 1 吗?我们定义 bh(x) 不含 x 本身、含 NIL。从 1 出发到它的左 NIL,路径是“1 → NIL”,不含 1、含 NIL,黑节点数 = 1(NIL)。所以 bh(1) = 1 正确。那从 3 出发到 NIL 的路径是“3 → 1 → NIL”,不含 3、含 NIL,黑节点是 1 和 NIL,共 2 个,不是 1!

我刚才的手算结论错了,现在纠正:bh(3) = 2,bh(1) = 1,bh(5) 呢?从 5 到左路 NIL:5 → 3 → 1 → NIL,不含 5,黑节点是 3?不,3 是红;黑节点是 1 和 NIL,共 2 个。所以 bh(5) = 2。右路:5 → 8 → 7 → NIL,黑节点是 7、NIL,共 2 个。一致!再看递推公式:bh(黑孩子) + 1:bh(1) + 1 = 2 = bh(3) ✓;bh(7) + 1 = 2 = bh(8) ✓;bh(3) + 1 = 3 ≠ bh(5)?又不对了——因为 5 的两个孩子 3、8 都是,不是黑孩子!所以公式的适用分支要选对:当孩子是红时,bh(x) = bh(红孩子) = 2;当孩子是黑时,bh(x) = bh(黑孩子) + 1。

我故意在这里展示一次“算错再纠正”的过程,因为它太典型了:黑高计算的出错点几乎全在“含不含自己”和“孩子是红还是黑”这两个地方。把口径彻底统一后,正确的递推是:

设 y 为 x 的任一孩子(NIL 的 bh = 0)。
若 y 是黑色:bh(x) = bh(y) + 1
若 y 是红色:bh(x) = bh(y)

再验算:bh(5) = bh(3) = 2(3 是红)✓;bh(3) = bh(1) + 1 = 2(1 是黑)✓;bh(8) = bh(7) + 1 = 2 ✓。全部自洽。下面这张图总结了“从下往上”的黑高传播规律,红色节点“不占层数”,黑色节点“每层 +1”:

黑高的递推:黑色 +1,红色不动 黑节点:孩子黑高 + 1 黑 x bh=k+1 bh(x) = bh(黑孩子) + 1 孩子 bh=k 黑色孩子自身 +1 红节点:孩子黑高不变 红 x bh=k bh(x) = bh(红孩子) 孩子 bh=k 红色孩子不计数

图 7:黑高递推的两条规则——经过黑色孩子要 +1,经过红色孩子原样继承,这是第 4.2 节手算的核心工具。

4.3 黑高与树高的关系

黑高和树高是两个不同的量:树高是“最长路径上的边数”,黑高是“路径上的黑色节点数”。因为红色节点不计数,红黑树的黑高往往明显小于树高。例如 2.1 那棵 9 个节点的树,树高(最长路径边数)是 3,而 bh(根) = 2。

有一个非常直观的关系值得记住:红黑树的高度 h 最多是黑高的两倍,即 h ≤ 2·bh(根)。理由就是红黑交替:任意路径上,红节点数不超过黑节点数,而黑节点数就是 bh(根)(加上根本身黑或红的微小差异)。这个不等式是第 5 节证明的发动机:只要能把 bh(根) 限制成 O(log n),树高就自动是 O(log n)。AVL 树没有颜色,它的“水平仪”是每个节点的高度;红黑树的“水平仪”是黑高——一个比高度更“粗犷”、更便宜维护的量。理解了黑高,红黑树的一半逻辑就已经通了。

4.4 黑高的另一层含义:子树的下界

黑高除了是“水平仪”,还直接给出“一棵子树至少有多少节点”。黑高为 b 的子树,内部节点数至少是 2ᵇ − 1。这个下界的直觉我们在 5.2 节会正式使用,这里先用小数字建立体感:

  • b = 0:空树(或只有 NIL),节点数 0,2⁰ − 1 = 0 ✓;
  • b = 1:至少 1 个节点——一个黑色叶子,左右各挂一个 NIL,2¹ − 1 = 1 ✓;
  • b = 2:至少 3 个节点——一个黑色根、两个黑色叶子(这是“纯黑满二叉树”的最小形态),2² − 1 = 3 ✓;
  • b = 3:至少 7 个节点——三层纯黑满二叉树,2³ − 1 = 7 ✓。

“至少”的意思是:红节点可以加塞更多节点,但绝不可能让黑高为 b 的子树少于 2ᵇ − 1 个节点。这个下界反过来看就是:如果树里有 n 个节点,那么黑高不可能超过 log₂(n+1)——因为再多一个黑高,就需要翻倍的节点来“撑起”它。这正是“平衡”的数学本质:节点的数量以指数速度兑换高度,所以高度只能是对数。

黑高的递推更新也值得预告一下:插入红色新节点时,所有路径的黑高都不变(红节点不计数),所以插入的“第一轮”天然不破坏性质五;真正让黑高“全局减一”的是删除黑色节点——某条路径少了一个黑,其他路径没少,性质五立刻破功,这正是第 12 篇“双黑修复”的起点。红黑树修复流程的一切复杂性,都源于“黑高局部失衡”和“红黑交替被打破”这两类伤害,而修复工具只有变色和旋转两把

5 关键定理的直觉版:红黑树为什么是 O(log n)

这一节回答红黑树最核心的问题:凭什么五条性质就能保证高度 O(log n)? 我们会分两步走,先证明“最长路径 ≤ 2 × 最短路径”,再证明“n 个节点时高度 ≤ 2·log₂(n+1)”。两步都采用“直觉 + 关键算式”的讲法,不追求完全严格的归纳证明——严格版本在任何算法教材里都有,本篇更想把“为什么”讲透。

5.1 第一步:为什么最长路径 ≤ 2 × 最短路径

设根为 r,黑高 bh(r) = b。考虑任意一条根到 NIL 的路径 P,路径上的黑节点数必须是 b(性质五),红色节点数记为 rp。性质四禁止连续红,所以红色节点不能相邻;在一个序列里,不相邻的红节点数至多比黑节点数多 1,更宽松地说,rp ≤ b(黑节点数 b 已经足够给红色节点“间隔开”)。于是:

路径长度(节点数) = 黑节点数 + 红节点数 ≤ b + b = 2b

再看最短路径:性质五保证所有路径黑节点数都是 b,而“全黑路径”是允许存在的(红节点是可选的,不是必须的),所以最短路径就是一条纯黑路径,长度恰好是 b(含终点 NIL 的计数约定下再调整常数,量级不变)。于是:

最长路径 ≤ 2b = 2 × 最短路径

这个证明只有两行,却把性质四和性质五的“分工”展现得淋漓尽致:性质五负责“所有路径黑节点数相同”(b 是公共常数),性质四负责“红节点不能扎堆”(rp ≤ b)。缺了任何一条,不等式都会失守——这正是第 3.4、3.5 节反复强调的“一横一纵”。

两条极端路径:最短全黑,最长红黑交替 最短路径:全是黑节点 NIL 长度 = b 全黑路径合法,它就是最短路径 最长路径:黑红交替 NIL 长度 ≤ 2b 最长 ≤ 2 × 最短

图 8:性质四、五合起来的直接推论——红节点被黑节点隔开,任意路径长度都夹在 b 与 2b 之间。

这张图把两条极端路径并排画在一起:一条“勤俭持家”全是黑,一条“借满夹层”红黑交替,后者至多是前者的两倍长。这就是红黑树平衡性的第一层直觉:树的形状允许歪,但歪的程度被锁死在“二倍”以内

5.2 第二步:为什么 n 个节点时高度 ≤ 2·log₂(n+1)

“最长路径 ≤ 2 × 最短路径”只说明“树不会特别歪”,但没说“树一定很矮”——万一最短路径本身就特别长呢?所以还需要把黑高 b 和节点总数 n 联系起来:b 越大,节点数越少?不,恰好相反——b 越大,树能装的节点越多;反过来,给定 n 个节点,b 不能太大

这一步的标准证明分两个小引理:

引理 A(黑高与高度的关系):h ≤ 2b,上一小节已经得到。

引理 B(黑高与最少节点数):一棵黑高为 b 的红黑树,至少包含 2ᵇ − 1 个内部节点(把 b 换成含根在内的计数时是 2^(b+1) − 1,常数差异不影响结论)。

引理 B 的直觉是这样:把红黑树里所有红色节点“忽略”掉,只看黑色节点,性质五保证每个黑色节点 x 的两侧(x 之下)黑高完全一致,于是黑色骨架本身是一棵“每层全满”的树——最少的黑节点排法就是“一棵没有红节点的纯黑满二叉树”。纯黑满二叉树的黑高是 b 时,节点数是 2ᵇ − 1(例如 b = 1 时至少 1 个黑节点,b = 2 时至少 3 个,b = 3 时至少 7 个)。红节点只会增加节点数,不会减少,所以任何红黑树都至少有 2ᵇ − 1 个内部节点。

把两个引理接起来:

n ≥ 2ᵇ − 1
⇒ b ≤ log₂(n+1)
⇒ h ≤ 2b ≤ 2·log₂(n+1)

这就是红黑树高度上界的完整链条。注意它从头到尾只用了性质四、性质五,以及“黑色骨架是最少节点时呈满二叉树”的观察——每一步都是直觉可及的,没有任何黑魔法。

为什么高度是 O(log n):两条引理接成一条链 性质五:所有路径黑节点数相同 = b (引理 B 的起点) 黑色骨架是满的 最少节点 2ᵇ − 1 引理 B:n ≥ 2ᵇ − 1 n ≥ 2ᵇ − 1 ⇒ b ≤ log₂(n+1) 黑高被节点数限制住 性质四:红节点不能连续 (引理 A 的起点) 红节点数 ≤ 黑节点数 ⇒ h ≤ 2b 引理 A:h ≤ 2b h ≤ 2·log₂(n+1) = O(log n) 两条引理在此汇合 黑高有界 + 红高有界

图 9:性质五把黑高 b 压成 O(log n),性质四把高度 h 压成 ≤ 2b,两段接起来就是红黑树的高度上界。

5.3 直觉与严格证明的分界线

细心的读者会注意到,引理 B 的“满二叉树”说法有一个隐含前提:性质五说的是“黑节点数相同”,而不是“黑节点分布在同一层”。严格地说,黑色骨架不一定真的是满二叉树——它可能左右不对称,只是两侧黑高相同。比如黑高为 2 时,最少黑节点数确实是 3(根 + 两个黑叶子),但也可以有 4 个黑节点(根、两个黑孩子、再加一个黑孙子),甚至更多。所以引理 B 的严格形式是“黑高为 b 的红黑树至少 2ᵇ − 1 个节点”,它的证明用归纳法:根的两侧黑高至少是 b−1(当根为黑)或 b(当根为红),两侧各自至少 2^(b−1) − 1 个节点,加上根,总数 ≥ 1 + 2·(2^(b−1) − 1) = 2ᵇ − 1。这个归纳正是“满二叉树”直觉的严格化:让节点数最少的排法,就是让每一侧都取最小,而最小排法逐层递归下去就长成满二叉树

同理,5.1 节的 rp ≤ b 在严格证明里需要小心处理路径起点(根可能是黑也可能是红)和终点(NIL 的计数),但修正后的不等式仍然是“最长路径 ≤ 2 × 最短路径”,只是常数略有调整。红黑树定理的本质不会因为这几处计数细节而改变,所以本篇采用“直觉版”:记住黑高一致、红黑交替、黑色骨架至少 2ᵇ − 1 这三个支点,红黑树的 O(log n) 就在你手里了

最后给出一个数值感受:n = 100 万时,2·log₂(n+1) ≈ 2 × 20 = 40,也就是说红黑树即使长到最坏情况,查找 100 万个键也只需要约 40 次比较;而同样 n 的退化 BST 需要 100 万次。40 对 100 万,这就是“平衡”的含金量。AVL 在这个规模下大约是 28 层,比红黑树矮一点——但正如第 6 节要讲的,矮一点的收益未必抵得过维护成本。

5.4 数字演练:从黑高一路推到树高

为了让第 5 节的推导“落地”,我们做一个完整的数字演练。假设一棵红黑树的黑高 bh = 20:

  1. 由“黑高为 b 的子树至少 2ᵇ − 1 个节点”,这棵树至少能装 2²⁰ − 1 = 1,048,575 个内部节点——超过 100 万;
  2. 由“最长路径 ≤ 2 × 最短路径”,树高(最长路径的边数)不超过 2 × 20 = 40;
  3. 结论:100 万个节点,最多 40 层,最坏查找 40 次比较

把规模放大到 10 亿个节点(现代数据库的日常):黑高 ≤ log₂(10⁹+1) ≈ 30,树高 ≤ 60,最坏查找 60 次比较。这个数字对内存里的红黑树来说已经非常可观;但对磁盘上的索引来说,60 次磁盘访问仍然太多——这正是数据库改用 B+ 树的原因之一:B+ 树用“多路节点”把 10 亿条记录的索引压到三四层,一次查找只有三四次磁盘 I/O。同样的“对数高度”逻辑,在内存和磁盘两种介质上推导出了不同的工程结论,这也是“树系列”后续几篇的伏笔。

顺便澄清一个常见错觉:40 层是最坏上界,不是“平均 40 层”。随机插入构造的红黑树,实际高度通常明显低于上界;而刻意构造的最坏输入(先按某种顺序插入,再不断删除触发最坏修复)才能逼近上界。工程上关心上界,是因为攻击者和极端负载专挑“上界路径”走;平均情况再漂亮,也不能替代最坏情况的承诺。

6 红黑树 vs AVL:一场务实的对比

6.1 高度上限:2·log₂(n+1) 对 1.44·log₂(n+2)

先给结论:AVL 的树更矮,红黑树的树稍高,但两者都是 O(log n),差距只是一个常数

  • AVL 树的高度上界 ≈ 1.44·log₂(n+2),这个数字来自斐波那契式的最少节点递推(第 9 篇推导过);
  • 红黑树的高度上界 ≤ 2·log₂(n+1),来自第 5 节的两步证明。

把两个上界放在一张图里看,红黑树的最坏高度大约是 AVL 的 1.4 倍:

同样 100 万个键的最坏树高 AVL 树 约 1.44 × log₂(10⁶) ≈ 28 层 红黑树 约 2 × log₂(10⁶+1) ≈ 40 层 退化 BST 100 万层 查找退化成 O(n) 红黑树最坏高度约为 AVL 的 1.4 倍,但两者都是 O(log n)

图 10:同一规模下,红黑树的最坏树高约是 AVL 的 1.4 倍,退化 BST 则彻底失去对数保证。

“40 层对 28 层”听起来红黑树输了不少,但要注意两点。第一,这是最坏上界,不是平均树高;随机数据下两棵树的高度差异远小于这个比值。第二,查找的成本不只是“层数”,还有每次比较和缓存命中的常数;28 层和 40 层的差距,在一次查找里是十几纳秒级别的差异,只有在“每秒百万次查找”的极端读密集场景下才可能被放大。红黑树用十几层的高度,换来了显著更低的写入维护成本——这笔交易划不划算,取决于负载,没有绝对答案。

6.2 旋转频率:谁更爱“动手术”

这是红黑树和 AVL 最本质的分歧。AVL 的平衡标准严(高度差 ≤ 1),所以每次插入、删除后“失衡”的概率高;红黑树的平衡标准宽(路径比 ≤ 2),所以触发修复的概率低、修复的幅度小。

精确数字来自经典教材与实验:AVL 插入平均需要约 0.5 到 0.6 次旋转(有时是双旋,代价更高),删除平均需要更多,且一次删除最多可以触发 O(log n) 次旋转;红黑树插入平均只需要约 0.2 次旋转,删除平均约 0.5 次旋转,但删除修复的旋转也以常数次为主,而且大量修复只需要变色。变色只改颜色位,不碰任何指针,成本远低于旋转。

一个更重要的结构性差异:AVL 删除的“失衡向上传染”可能让一次删除在沿途每一层各转一次;红黑树删除虽然也有向上传播(叔叔为红的“变色传播”),但传播的是变色,旋转只在特定分支发生,摊还下来仍然接近常数。下面这张表把“维护动作”拆开对比:

操作AVL 插入红黑树插入AVL 删除红黑树删除
平均旋转次数约 0.5–0.6约 0.2可达 O(log n)常数次
变色次数0(不涉及)常数次0(不涉及)常数次
向上传播最多 1 处旋转(插入)变色传播 + 最多 2 次旋转多处可能变色传播 + 常数次旋转
维护数据高度/平衡因子逐层更新颜色位,局部更新同左同左

一句话总结:AVL 是“写时多干活、读时占便宜”,红黑树是“写时少折腾、读时略吃亏”。如果你的系统里写操作是常态,红黑树的优势是实打实的。

6.3 查找、插入、删除:分别谁快

把三个操作拆开看,结论并不像“红黑树全面胜出”那么简单:

查找:AVL 略快。树更矮,比较次数更少,最坏情况下差距约 1.4 倍,平均情况下差距小得多。如果数据全在内存、键是整数,AVL 每轮少比较一两次;如果键是比较昂贵的字符串,这个差距会被放大。纯查找场景,AVL 是更好的选择。

插入:红黑树通常更快。插入本身都是 O(log n) 的“先找空位再挂节点”,差别全在修复阶段。红黑树的插入修复平均旋转少、且大量是变色;AVL 的插入修复虽然最多一次旋转,但“发现失衡”需要逐层更新高度并检查平衡因子,每层都做,常数开销更大。实验数据普遍显示红黑树插入吞吐更高。

删除:红黑树优势更明显。AVL 删除最坏要 O(log n) 次旋转,红黑树删除虽然实现复杂(第 12 篇见真章),但旋转次数以常数为主,大部分情况靠变色收尾。写密集、增删交替频繁的场景,红黑树几乎总是赢家。

选型的第一步:先问负载画像 你的负载是什么? 读写比例决定答案 读多写少 写多/均衡 读多写少 (如只读缓存、字典) 构建后几乎只查 写多读少或读写均衡 (如标准库容器、调度器) 增删频繁、读写混合 选 AVL:树更矮,查找更快 纯读场景 AVL 更划算 选红黑树:插入删除维护更便宜 写入不拖后腿,通用场景更稳

图 11:负载决定选型——读多写少偏向 AVL,写多或读写均衡偏向红黑树,标准库无法假设负载才默认选后者。

6.4 工程选型:为什么 Java、C++ 和 Linux 都选了红黑树

理论对比之后,看三个真实世界的“投票”:

Java 的 TreeMap / TreeSet:JDK 的 java.util.TreeMap 使用红黑树实现(TreeSet 底层就是 TreeMap)。它提供按键排序的 Map 语义,要求所有操作 O(log n),并且支持区间遍历。红黑树的“写友好”特性让它在增删改查混合的通用场景里表现稳定。

C++ 的 std::map / std::set:C++ 标准库的有序关联容器基于红黑树(libstdc++、libc++ 实现细节略有差异,但都是红黑树或其近亲)。标准库的承诺是“最坏情况 O(log n)”,且迭代器在插入后保持有效——红黑树的局部调整恰好满足这个迭代器契约(AVL 的旋转同样局部,但调整更频繁)。

Linux 内核:内核里红黑树无处不在:CFS 调度器用红黑树管理就绪进程,按虚拟运行时间排序;高效定时器、内存管理(如 vma 区间树)也用红黑树。内核场景的特征是:写操作(进程唤醒、定时器插入)极其频繁,节点会被反复插入删除,而查找也有硬实时要求。红黑树的写友好 + 最坏有界,让它成为内核的默认选择。

还有谁? Redis 的有序集合用跳表(第 15 篇讲),数据库用 B+ 树(第 13、14 篇讲),Python 的 dict 是哈希表。每种选择都对应不同的负载特征——没有“最好的平衡树”,只有“最适合当前场景的平衡树”

为什么这些工程巨头不选 AVL?不是说 AVL 不好,而是它们的负载都不是“纯读”。标准库无法假设调用者读多写少,内核面对的是高频率的增删,红黑树在“最坏情况有界”和“写入成本可控”之间找到了更好的平衡点。反过来,如果你写一个只读字典、构建完不再修改的索引,AVL(甚至直接排序数组)会更好。工程选型的本质,就是认清自己的读写比例。

工程界的三个“投票”:为什么都选红黑树 Java TreeMap 排序 Map / 通用容器 通用读写均衡 最坏 O(log n) 增删改查混合稳定 C++ std::map 有序关联容器 迭代器契约 + 写性能稳定 插入后迭代器不失效 Linux 内核 CFS 调度器、定时器、vma 增删高频 硬实时上界 最坏情况有保证 红黑树:写友好 + 有界 读写都稳,写入不拖后腿

图 12:Java、C++ 与 Linux 内核的负载各不相同,却都指向同一个结论——红黑树是“通用均衡”而非“全面碾压”。

6.5 别忘了场景的下限:小数据、缓存与并发

AVL 与红黑树的“谁更好”之争,默认前提是“数据量足够大、操作足够频繁”。但如果数据量只有几百个键,或者应用对延迟的敏感度很低,这个争论就失去了意义——两个结构的最坏高度差不过几层,一次比较的差别是纳秒级,而实现一个红黑树删除修复的代价是几天的调试时间。复杂度分析回答“能不能用”,工程判断回答“值不值得用”。几百个键的排序字典,一个排序数组加二分查找可能比任何平衡树都快、都省内存。

缓存与并发视角也值得补一笔。树状结构天然是“指针跳跃”的:查找要访问分布在内存各处的节点,缓存命中率不如数组。红黑树和 AVL 在这个问题上半斤八两,真正的赢家是 B 树(把多个键打包进一个节点)和紧凑数组。并发场景下,红黑树修改的“局部性”意味着锁的粒度可以比较小,但实现线程安全仍然复杂;跳表因为天然支持无锁化,反而成了高并发有序集合的宠儿——Java 的 ConcurrentSkipListMap 就是例子。所以工程选型从来不是“红黑树 vs AVL”的二元题,而是在数据规模、读写比例、内存布局、并发模型、实现成本五个维度上的综合决策

7 插入/删除修复预览:叔叔的颜色是分水岭

7.1 插入修复的总览:先变色,后旋转

红黑树的插入分两步:第一步和普通 BST 完全一样——查找空位、挂上新节点;第二步是修复颜色,让五条性质重新成立。经典实现把新节点初始化为红色,为什么?因为红色节点“不占黑高层数”,初始为红不会破坏性质五(黑高一致)——所有路径的黑节点数都没变。它可能破坏的只有性质二(如果新节点是根)和性质四(如果父节点也是红)。于是插入修复只需对付“红-红”冲突,这是整个红黑树实现里最著名也最优雅的部分。

处理“红-红”冲突时,重点看新节点 z 的叔叔(父节点的兄弟节点)的颜色:

  • 叔叔是黑色(或叔叔是 NIL):说明“红色夹层”撞上了正式楼层,无法靠“把颜色往上推”解决,必须旋转——把夹层摆正,让红黑重新交替;
  • 叔叔是红色:说明夹层上方还有一层夹层(祖父是黑,父亲和叔叔都是红),此时可以把两个红孩子同时染黑、祖父染红,把“红-红”冲突向上推一层,然后对祖父重新检查——这就是变色传播

为什么叔叔的颜色是分水岭?因为叔叔的颜色决定了“可不可以只改颜色就解决”。如果叔叔是红,把父亲和叔叔一起变黑、祖父变红,路径上的黑节点数不变,性质五保持,冲突只是“上移”了;如果叔叔是黑,变黑父亲会打破黑高(父亲的子树多了一个黑节点),必须用旋转把结构理顺,同时保留黑高。这个“变色或旋转”的二选一,正是红黑树“维护成本低”的根源:大多数冲突都能用变色向上推走,旋转是少数情况

7.2 插入修复的四种情形(预览版)

设新节点 z 为红,父节点 p 为红(否则无需修复),祖父 g 必为黑(性质四),看叔叔 u 的颜色和 z 的位置:

  1. u 为红:p、u 染黑,g 染红,把 z 上移到 g,继续循环;
  2. u 为黑,z 是 p 的外侧孩子(LL 或 RR 型):对 g 做一次旋转(右旋或左旋),p 染黑、g 染红,结束;
  3. u 为黑,z 是 p 的内侧孩子(LR 或 RL 型):先对 p 做一次旋转,把 z“转成”外侧孩子,再按情形 2 处理(一次双旋),结束。

情形 1 循环向上,最坏走到根;情形 2、3 各最多两次旋转后必然结束。平均而言,情形 1 占据大多数,所以红黑树插入的旋转次数很少。第 12 篇会把每一种情形画成“前/中/后”三张图并给出完整代码,这里先记住两句话:变色负责“向上推”,旋转负责“就地摆平”;推得动就变色,推不动就旋转

插入修复的决策树:叔叔颜色是总开关 z 为红,父 p 为红 进入修复循环 叔叔 u 的 颜色? u 为红 情况一:变色上溯 p、u 染黑,g 染红 冲突上移,继续循环 u 为黑 z 在 p 的 哪一侧? 外侧 情况二:一次解决 旋转 g + 变色 (LL / RR) 内侧 情况三:双旋 先旋转 p 掰直 再旋转 g + 变色 记住顺序:先看叔叔颜色,再看 z 的位置

图 13:插入修复的三条分支——叔叔为红就变色上推,叔叔为黑再按直线/折线决定一次旋转还是两次旋转。

7.3 删除修复:为什么更难

插入修复的“红-红”冲突是明牌:新节点是红的,一眼就能看到矛盾。删除修复则相反——删除一个节点可能让某条路径上少了一个黑节点,破坏的是性质五(黑高一致),而黑高不一致是“看不见”的:没有一条明显的“红色冲突”告诉你哪里错了。所以红黑树删除需要引入一个巧妙的替身:把“被删的黑”转嫁给接替它的节点,让那个节点变成“双重黑”(或“黑+黑”),然后围绕“双黑”做修复。

删除修复同样以“兄弟节点的颜色”为分水岭,四种情形(兄弟为红、兄弟为黑且侄子全黑、兄弟为黑且有红侄子……)第 12 篇逐一拆解。这里只预告三个关键直觉:

  1. 删除的是红节点:直接删,不影响黑高,无需修复(这是红黑树偏爱红色的隐藏福利);
  2. 删除的是黑节点:路径上少了一个黑,必须用“双黑”标记失衡点,然后向兄弟“借”黑色;
  3. 兄弟是红的:先旋转把兄弟转成黑,把问题转化到更简单的子情形——旋转的目标永远是“把未知情形变成已知情形”

插入和删除的修复共用同一套旋转原语,但删除的传播方向更复杂、情形更多。这就是为什么几乎所有教材都把“红黑树删除”列为数据结构课程最难的三件事之一——第 12 篇我们会用状态机式的图解把它彻底讲透,本篇先建立“叔叔/兄弟的颜色决定分支”的框架。

7.4 变色 vs 旋转:红黑树维护成本的秘密

最后用一个对比图收尾。变色和旋转是红黑树仅有的两种“修理工具”,它们的成本完全不同:

红黑树只有两把修理工具:变色与旋转 变色(便宜) 只改颜色位 指针、子树结构不变 所有路径黑高不变 旋转(较贵) 改写 3 条父-子指针 子树根可能改变 需要重新挂回父节点 设计目标:尽量变色 万不得已才旋转

图 14:变色零指针操作、黑高守恒;旋转要改结构——红黑树的低成本秘密就是“多用变色、少用旋转”。

红黑树之所以能在写入密集场景胜出,秘密就在于它把大部分修复工作都变成了变色:插入的“叔叔为红”分支、删除的“双黑传播”分支,全是变色操作,旋转只出现在少数结构性错误里。AVL 没有变色这个工具,每次失衡都只能旋转——这是两者维护成本差异的根本来源。

8 常见误解澄清

8.1 误解一:红黑树一定比 AVL 快

这是流传最广的误解,必须第一个澄清。红黑树不是“一定比 AVL 快”,而是在“写入多、读写均衡”的通用场景里通常更划算。纯查找场景,AVL 的树更矮,比较次数更少,往往更快;只读字典、构建后不修改的索引,AVL 是合理甚至更好的选择。红黑树的优势集中在插入、删除的维护成本上,而不是查找本身。

为什么标准库普遍选红黑树?因为标准库要服务未知的调用者:不知道你的读多还是写多,不知道你的键是比较快的整数还是昂贵的字符串,不知道你是低频操作还是每秒十万次。在这种“无法预判负载”的前提下,选择一个“读写都稳、写入不拖后腿”的结构是理性的;而红黑树恰好是那个“各项都不差”的均衡选手。选型的前提是负载画像,没有画像就没有“谁更快”的答案

误解一:红黑树一定比 AVL 快? 红黑树一定比 AVL 快? 流传最广的误解 不:取决于负载 没有画像就没有答案 读多写少 写多/均衡 读多写少 → AVL 更快 树更矮、比较更少 写多/均衡 → 红黑树更划算 写入维护成本更低 结论:红黑树是“通用均衡”, 不是“全面碾压”

图 15:负载决定胜负——读多写少 AVL 更快,写多或读写均衡红黑树更划算,标准库选择后者只因无法预判负载。

8.2 误解二:NIL 叶子是“可以忽略的细节”

很多初学者画红黑树时直接省略 NIL,认为它只是“为了好看”的装饰。这是危险的误解——NIL 是红黑树定义的一部分,不是可选项。性质三(NIL 是黑的)和性质五(黑高一致)都依赖 NIL 作为统一终点;省略画法只是“画图省事”,在验证性质时必须手动把每个空位当成黑色 NIL 补回来。

举一个具体例子:一棵只有根节点 5 的红黑树,省略画法里它孤零零一个黑点,看起来性质五“无需验证”。显式画法里它有左右两个黑色 NIL,从根到左 NIL 的黑节点数是 2(根 + NIL),到右 NIL 也是 2,性质五成立。如果 NIL 不算黑,性质五就变成“路径上黑节点数为 0 或 1”,计数基准立刻混乱。NIL 不是细节,它是让“每条路径”有确定终点的数学脚手架

工程实现里,NIL 通常被实现为一个共享哨兵对象 nil,所有空指针都指向它;查找、插入、删除代码里“节点 == nil”统一处理。这个设计让红黑树代码比“判空版”更整洁,也再次说明 NIL 是实打实参与运行的,不是纸面概念。

8.3 误解三:红黑树是一种“全新”的数据结构

红黑树看起来和 BST、AVL 是三个不同的物种,但它的血缘比想象中近:红黑树本质上是 2-3-4 树(也叫 4 阶 B 树)的二叉树编码。2-3-4 树的每个节点可以容纳 1 到 3 个键,拥有 2 到 4 个孩子,所有叶子在同一层——它天生完美平衡。红黑树把 2-3-4 树的“多键节点”拆成若干个通过红色连接串联的二叉节点,黑色节点是“真节点”,红色节点表示“我和我的黑父节点原本属于同一个 2-3-4 节点”。

这个对应关系解释了很多“为什么”:

  • 为什么红色节点不能连续红?因为连续两个红色连接意味着“三个键挤在一个 2-3-4 节点里”,而 2-3-4 节点最多 3 个键、4 个孩子,对应到二叉编码里恰好是“1 个黑 + 最多 2 个红”——再多就超过 4 阶了;
  • 为什么每条路径黑节点数相同?因为 2-3-4 树所有叶子在同一层,黑色节点对应“真正的层”,所以黑高一致;
  • 为什么插入时“叔叔为红”就变色?因为叔叔为红意味着祖父节点是一个“满的 2-3-4 节点”(含 3 个键),插进第 4 个键就要“分裂”——分裂在二叉编码里就是父节点上移、兄弟节点染黑、祖父染红。
同一个逻辑节点:2-3-4 树 vs 红黑树编码 2-3-4 树:一个节点装 3 个键 [a | b | c] 左子树 中子树 右子树 拆开 红黑树:同一个节点的二叉树编码 b a c 左子树 NIL NIL 右子树 一个 3 键节点 = 一个黑节点 + 两个红孩子

图 16:2-3-4 树的“一节点三键”在红黑树里编码成“黑 b + 红 a + 红 c”,这正是红色节点含义的最终解释。

这个“2-3-4 树视角”是理解红黑树修复流程的终极钥匙:插入、删除的所有情形,几乎都能翻译成 2-3-4 树里“节点满了要分裂”“节点空了要合并”的动作。第 12 篇讲到删除时,我们会频繁回到这个视角。它同时也预告了第 13、14 篇的 B 树——2-3-4 树就是 B 树的 4 阶特例,而 B 树是数据库索引的核心。红黑树、2-3-4 树、B 树,其实是一棵家族树上的三个分支。

8.4 其他几个高频误解

“红黑树查找是 O(log n),插入也是 O(log n),所以两者一样快。” 错。复杂度只刻画增长趋势,不刻画常数;红黑树插入的常数明显大于查找(要修复颜色),而且查找可以走无锁只读路径,插入要写节点。工程上“都是 O(log n)”只是底线,常数决定了真实差距。

“红黑树一定需要每个节点多存一个颜色,所以空间开销大。” 错。颜色只需 1 个比特,实际实现常常用指针的最低一位编码颜色,空间开销几乎为零;对比 AVL 需要存高度或平衡因子(通常一个 int),红黑树在空间上反而更省。

“红黑树只有五条性质,实现很简单。” 错。五条性质只是定义,从定义到“插入删除后恢复性质”的修复算法是另一套复杂度;尤其是删除的四种情形和“双黑”处理,是数据结构领域公认的高难度内容。定义简单 ≠ 实现简单。

“红黑树和 2-3-4 树完全等价,可以随便互相替换。” 这个说法需要限定:经典的“红黑树 ↔ 2-3-4 树”对应关系建立在“红色节点只能作为左孩子”的附加约定上(左倾红黑树),通用红黑树允许红色左右孩子,对应的是“2-3-4 树 + 多种拆分方式”。两者思想等价,但具体实现细节并不一一对应。学习时用 2-3-4 树找直觉,写代码时以红黑树本身为准,这是最稳的姿势。

8.5 面试与考试视角:红黑树到底考什么

红黑树是算法面试和数据结构课程的“常驻嘉宾”,但绝大多数考察点都可以归到本篇讲过的几条线上。第一类是定义题:五条性质是什么、黑高怎么算、NIL 为什么必须存在——只要把本篇第 3、4 节吃透,这类题基本送分。第二类是推导题:证明最长路径不超过最短路径的两倍、推导高度上界——答案就在第 5 节的两步直觉里,面试官想听的不是背诵,而是“性质四管红、性质五管黑”的分工逻辑。第三类是权衡题:红黑树和 AVL 怎么选、为什么标准库选红黑树——第 6 节的负载分析就是标准答法。第四类是手写题:实现插入修复、说出叔叔颜色的分叉——这需要第 12 篇的完整代码训练,但框架(变色上移 vs 旋转摆平)本篇已经给出。

还有一个常被忽视的“软考点”:红黑树与 2-3-4 树的对应关系。能说出“红色节点是 2-3-4 节点内部的键、黑色节点是节点边界”的人,和只会背五条性质的人,在面试官眼里完全是两个层次。本篇第 8.3 节已经把这条血缘讲透,第 13 篇 B 树登场时你会发现,这条路居然一路通到了数据库索引。红黑树不是孤立的考点,它是“平衡树家族”承上启下的一环——这也正是“树系列”把它安排在第 11 篇的原因。

9 动手体验:在可视化实验室里观察平衡树

理论的终点是手感。本系列的搜索算法可视化实验室支持插入、删除与多种树形切换——如果实验室已支持红黑树,试着连续插入升序序列 1、2、3、…、30,观察红黑树如何通过变色和旋转把“必死的有序输入”重新掰回平衡;再插入 30 个随机键,对比树高和调整次数。重点观察两个现象:第一,红黑树的高度远小于插入次数,有序输入再也不能把树喂成链;第二,修复过程中“变色”出现得远比“旋转”频繁——这正好印证第 7 节的结论。

建议做一个纸笔实验:画一棵只有根节点的红黑树(根为黑,带两个 NIL),然后依次插入 2、3、4,亲手走一遍“新节点为红 → 叔叔为红 → 变色上移 → 到根染黑”的流程;再插入 1,走一遍“叔叔为黑 + 外侧孩子 → 旋转 + 变色”的流程。两个流程走完,你就能直观感受到“变色向上推、旋转就地摆平”的分工。第 12 篇会把全部情形系统化,本篇先让手和眼睛记住这两种动作的体感。

再做一个删除方向的纸笔实验:从一棵合法的红黑树里删除一个黑色叶子,观察它所在路径的“黑高缺了一格”;然后按第 7.3 节的预告,尝试让接替它的 NIL 变成“双黑”,再向兄弟“借黑色”或者旋转。你会发现删除修复的每一步都在回答同一个问题:怎么把“少了一个黑”的影响,用变色和旋转平摊回整棵树。这个实验做一遍,第 12 篇读起来会轻松一半。

10 五条性质速查表

性质规则内容一句话为什么
性质一每个节点非红即黑两种状态足以区分“正式楼层”和“借来的层”
性质二根是黑色树顶必须是正式楼层,且把根染黑永远免费
性质三NIL 叶子是黑色为“每条路径”提供统一终点,黑高计数才有基准
性质四红节点的孩子都是黑禁止连续红,红色夹层不能叠夹层,锁死路径比
性质五任一节点到后代叶子的路径黑节点数相同黑色骨架完美平衡,树高才有 O(log n) 上界
衍生结论内容
最长路径 ≤ 2 × 最短路径性质四 + 性质五的直接推论
高度 ≤ 2·log₂(n+1)黑高 ≤ log₂(n+1) + 路径比 ≤ 2 的合成
插入修复分水岭叔叔的颜色:红 → 变色上移;黑 → 旋转摆平
与 AVL 的取舍AVL 树更矮、查找略快;红黑树写入维护更便宜
工程默认选择Java TreeMap、C++ std::map、Linux 内核
隐藏血缘红黑树是 2-3-4 树的二叉树编码

11 自测题

已作答 0 / 6

下面六道题覆盖本篇的核心内容,建议先独立思考,再对照答案。

第 1 题:一棵红黑树里有 100 万个节点,它的高度最坏大约是多少?请写出推导所依赖的两个关键性质,并解释它们各自的作用。

第 2 题:红黑树插入的新节点为什么初始化为红色?如果初始化为黑色,会立刻破坏哪条性质?为什么?

第 3 题:为什么“红节点的孩子都是黑”加上“黑高一致”就能推出最长路径不超过最短路径的两倍?请用黑高 bh 写出不等式。

第 4 题:AVL 树和红黑树,在“读多写少”和“写多读少”两种负载下分别应该选谁?为什么标准库普遍选红黑树?

第 5 题:NIL 在红黑树里扮演什么角色?如果把 NIL 当成红色,哪些性质会失效?请具体指出。

第 6 题:红黑树插入修复时,“叔叔为红”和“叔叔为黑”分别对应什么动作?为什么叔叔的颜色决定了能不能只用变色解决?

12 下一篇预告

下一篇,《树系列第 12 篇:红黑树的插入与删除》,要把本篇的框架变成可以运行的代码和可以推演的状态机。你会看到:插入修复的三种情形如何用叔叔的颜色分叉、每种情形旋转前后子树怎么搬家;删除时“双黑”替身如何诞生、兄弟的颜色如何决定四种修复分支;以及完整的 TypeScript 实现、逐行注释和一组精心构造的测试序列。到那时,你会对“红黑树为什么工程上这么能打”有一个从定义到实现的全景理解。第 12 篇见!