高级数据结构-04:红黑树
红黑树
约束
红黑树是一类带颜色标记的二叉搜索树。它将外部节点(代码中的 NULL 引用)视为叶子节点,使整棵树在逻辑上成为真二叉树。合法红黑树满足以下约束:
- 树根约束:根节点为黑色。
- 外部节点约束:所有外部节点,即叶子末端的 NULL 空节点,均被显式或隐式定义为黑色。
- 红色节点约束:如果内部节点为红色,则其父节点和孩子节点都必须为黑色,因此任意根到叶路径上不能出现连续红节点。
- 黑深度约束:从任意节点到其所有后代外部节点的简单路径上,经过的黑色节点数相等。该数量称为该节点或子树的黑高度。

红黑树可以视为四阶 B-树(即 2-3-4 树)在二叉树形态下的一种等价实现。通过拓扑变换,每一棵红黑树都可以被映射为一棵四阶 B-树。
将红黑树中的红色节点在逻辑上向上提升,使其与黑色父节点处于同一高度。由黑色父节点和红色孩子组成的局部分支,会被压缩为一个 B-树内部节点。规则 3 限制红色节点不能连续相邻,规则 4 保证各路径黑深度相等,因此这种合并结果对应一棵平衡的四阶 B-树。

根据黑节点与红孩子的数量、位置,红黑树局部结构可分为以下四类,并分别对应四阶 B-树中的内部节点状态:
| 红黑树局部形态 | 关键码数量 | 对应四阶 B-树节点 | 结构与等价性 |
|---|---|---|---|
| 单黑节点(无红孩子) | 1 个 | 2-节点 | 孤立的黑节点,两侧的指针分别指向较小的子树。 |
| 黑节点 + 左侧单红孩子 | 2 个 | 3-节点 | 左倾的红黑节点对,在 B-树中被视为包含两个关键码的单体节点。 |
| 黑节点 + 右侧单红孩子 | 2 个 | 3-节点 | 右倾的红黑节点对,同样对应于 B-树中的 3-节点。 |
| 黑节点 + 左右双红孩子 | 3 个 | 4-节点 | 满载的超级节点,在四阶 B-树中达到容量上限,若再插入则会引发节点分裂(上溢)。 |
红黑树中的重染色与旋转,可以理解为 B-树动态更新中的节点分裂、合并与借用。红黑树用二叉结构表示多叉 B-树节点,同时保留对数级渐近复杂度。
树高边界
红黑树属于自平衡二叉搜索树(BBST),可以保证基本操作在最坏情况下仍保持对数级。包含 个内部节点的红黑树 ,其高度 被限制在 。

插入算法
双红
执行插入时,算法先按普通二叉搜索树规则查找插入位置,并将新关键码 挂到底层成为新叶子节点,记为 。为了不改变各路径黑高度,新插入节点默认染为红色。
节点染红后,规则 1、2、4 仍能保持。但如果新节点 的父节点 也是红色,就出现连续红节点,违反规则 3。该状态称为双红,即 。

新节点默认染红,是因为红节点不会增加任何根到外部节点路径上的黑节点数量,因此不会破坏最难维护的黑高度约束。如果一开始把新节点染黑,那么只有经过该新叶子的路径黑高度增加,修复范围会更大。也就是说,插入算法选择先保住规则 4,再处理可能出现的规则 3 双红冲突。
双红发生后,需要沿路径向上执行修正。算法考察四个局部节点:冲突红节点 、红父节点 、祖父节点 (由于 为红, 必然存在且为黑),以及叔父节点 。根据叔父节点 的颜色,双红修正分为两类。
以下为泛型面向对象层面的插入代码实现,展示了搜索、节点创建以及调用双红修正的过程。
1 | // 插入 e,并返回树中保存 e 的节点位置。 |
叔父节点为黑(RR-1)
当叔父节点 为黑色时(外部 NULL 节点也视为黑色),即 ,可以通过 Zig-Zig 或 Zig-Zag 方式处理。


映射到四阶 B-树模型中,该情形等价于在已有 3-节点中插入新的红关键码,使原黑关键码不再位于节点中央,形成 RRB 或 BRR 结构。
此时执行局部 3+4 重构。无论 当前形成 zig-zig 还是 zig-zag,重构都按中序顺序重新排列三个关键码,并将颜色调整为“红-黑-红”。
在指针调整上,居中并提升为局部子树新根的节点 染为黑色,其左右孩子 与 染为红色。从 B-树视角看,该过程将偏向一侧的三个关键码重组为居中分布。调整后双红被消除,局部最高点为黑色,黑高度不变,因此缺陷不会继续向上传播。该分支只需 次旋转和重染色。


RR-1 可以局部终止,是因为旋转后的局部根为黑色,且这一小块子树对外呈现出的黑高度与修复前保持一致。外层祖先只看到“这棵子树还是同样黑高度的一棵合法红黑子树”,因此不需要继续向上调整。
叔父节点为红(RR-2)
当叔父节点 为红色时,即 ,在 B-树模型中表示原超级节点已经包含两个红关键码和一个黑关键码。新的红关键码 插入后,该超级节点将包含 、、、 四个关键码,超过四阶 B-树单节点最多 3 个关键码的容量,等价于触发上溢。
算法通过模拟 B-树节点分裂处理该上溢:
-
将发生冲突的父节点 与红叔父节点 同时由红色翻转为黑色。在 B-树中,这等价于将原节点分裂为两个低层节点。
-
将居中的祖父节点 由黑色翻转为红色。等价模型中,这表示将居中关键码 向上进位,并合并到父层节点。

操作后,祖父节点 变为红色。若 的父节点也为红色,则分裂产生的新关键码在上一层继续引发冲突,即上溢向上传递。此时只需将 视为新插入的红色节点,通过尾递归或迭代继续执行双红修正。
该过程持续到双红冲突被消除,或上升到树根。若节点 到达根位置,按照规则 1 需要将其转回黑色。这对应 B-树根节点分裂,也是红黑树全局黑高度增加一层的唯一途径。
以下为 solveDoubleRed 方法源码。
RR-2 的本质不是“旋转”,而是“颜色翻转”。父亲和叔父变黑,相当于 B-树中分裂出的两个低层节点;祖父变红,相当于把中间关键码向父层上送。由于这个上送动作可能让上一层也装不下,所以 RR-2 才有可能继续向上传播。
1 | // 修复由节点 x 引发的双红冲突。 |
算法复杂度
执行时间开销的评估,详见下表:
| 节点状态条件 | 局部旋转次数 | 节点染色次数 | 后续系统状态与调整走向 |
|---|---|---|---|
| u 为黑 | 1~2 次 | 2 次 | 重构消除冲突,黑高度不变,整个红黑树调整随即完成。 |
| u 为红 | 0 次 | 3 次 | 无旋转操作;可能在祖父处再次出现双红冲突,但必然向上攀升两层。 |
重构与染色均为常数时间,向上递进层数受树高 限制。因此,RedBlack::insert() 的整体运行时间为 。插入过程中最多发生一次局部重构,即 1 到 2 次旋转,其余为自底向上的常数级染色迭代。
构建实例
下面将数值 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 按升序插入一棵初始为空的红黑树。该序列会使普通二叉搜索树退化为链表,但红黑树可通过旋转与染色维持树高约束。
-
Insert 1:节点被作为叶子插入并染红。系统立即意识到它是全树的 root,因此根据规则 1 将其翻转回黑色。

-
Insert 2:作为节点 1 的右孩子插入并染红。其父节点(1)是黑色,不存在双红冲突,操作完成。

-
Insert 3:作为节点 2 的右孩子插入并染红。此时引发双红(3 的父节点 2 是红色)。检查叔父节点(NULL,故为黑色)。此时判定为同侧的 zig-zig 情况。算法进行单旋(Rotate P around G,即节点 2 绕节点 1 左旋),并将父节点 2 染黑,祖父节点 1 染红。至此,节点 2 成为根,1 和 3 分别为其左右红孩子。

-
Insert 4:由于示例采取特定的自顶向下遍历调优策略,在向下遍历寻找插入点时,提前发现节点 2 带有一对红孩子(1 和 3)。为避免随后的上溢,系统实施预发性翻转:将节点 2 染红,将 1 和 3 染黑。接着发现 2 是树根,立刻将其转回黑色。随后节点 4 顺利作为 3 的右红孩子插入,其父节点(3)为黑色,无冲突产生。

-
Insert 5:作为 4 的红孩子插入。父节点 4 为红,产生双红。节点 5 相对于祖父(3)处于外侧。叔父(节点 1 的右侧空孩子)为黑。系统执行父辈和祖父辈之间的旋转(节点 4 绕节点 3 左旋),并重新染色,最终 4 成为子树新根且为黑色,3 和 5 为其左右红孩子。

-
Insert 6:向下遍历路径中发现节点 4 带有双红孩子(3 和 5)。触发向下途中的预染:节点 4 转红,3 和 5 转黑。节点 4 的父节点(2)是黑色,不引发上层问题。随后节点 6 作为 5 的红孩子插入,父节点(5)已黑,直接完成插入。

-
Insert 7:作为 6 的红孩子插入。引发双红。执行旋转(6 绕 5 左旋)和染色。节点 6 成为黑色子树根,5 和 7 为其左右红孩子。


-
Insert 8:下探途经节点 6,发现其拥有双红孩子(5 和 7)。预染:6 变红,5 和 7 变黑。然而,节点 6 的父节点(4)此时也是红色的。系统必须针对这起在更高中层爆发的双红执行旋转。此时节点 6 位于祖父节点 2 的右树外侧,通过将 4 绕 2 进行左旋,并配合染色,完成全树结构的修复。随后节点 8 顺利在底端作为 7 的红孩子插入,无冲突。


-
Insert 9:下探时发现节点 4 拥有双红孩子(2 和 6)。预染:4 变红,2 和 6 变黑。由于 4 此刻是树的根节点,系统随即将其转回黑色。9 被作为 8 的红孩子插入,引发底层双红,经过常规旋转与染色修正。


-
Insert 10:下探发现 8 有双红孩子(7 和 9),执行预染(8 变红,7 和 9 变黑)。此时 8 的父节点(6)为黑,无异常。10 作为 9 的红孩子插入,父节点(9)为黑,操作完成。

-
Insert 11:作为 10 的红孩子插入,引发底层双红。通过单次旋转与染色完成修正。

通过这 11 步递增序列测试可以看到,红黑树能够持续维持高度约束,并通过节点颜色翻转(预染机制)减少向上传递的连续旋转。与追求高度差不超过 1 的 AVL 树相比,红黑树允许局部路径长度比达到 1:2,从而换取更低的结构维护成本。

重构与实现
“3+4”重构
处理 RR-1 双红冲突时,算法依赖局部 3+4 重构。该机制统一处理二叉树旋转的四类基础对称情况(Left-Left, Left-Right, Right-Left, Right-Right)。当红黑树局部违规时,涉及三个节点 、、,以及与它们相连的四棵内部子树 到 。
所谓“非法”,指的是在某个 3-节点中插入红关键码后,原黑关键码不再处于居中位置。无论经过单次旋转(如 zig-zig)还是双次旋转(如 zig-zag),最终目标都是按中序顺序将三个节点分配为局部根及其左右孩子,再统一套用颜色规则。
C++ STL 红黑树
C++ 的标准模板库 STL 中的红黑树实现采取了较扁平的指针操作。以下提取自 STL 的 _Rb_tree_rebalance 源码片段,它印证了上述理论,并在细节上做出了性能层面的权衡:
1 | inline void _Rb_tree_rebalance(_Rb_tree_node_base* _x, _Rb_tree_node_base*& _root) { |
这段 STL 源码没有额外封装 3+4 重构函数,而是先判断节点是否为 zig-zag(异侧)形态;若是,则先执行一次单旋,将其转换为 zig-zig,再通过一次反向单旋完成修正。这种内联式写法减少了跨函数调用开销。
删除算法
目标节点摘除与双黑
相比插入,删除更容易破坏红黑树的黑高度约束。算法先按普通二叉搜索树逻辑执行 removeAt(x, _hot),定位并摘除目标节点 。如果实际被摘除节点为红色,整树黑高度不变,规则 3 和规则 4 均保持成立,删除可以直接结束。
若被删除节点与其直接接替者 构成一红一黑配对,只需将接替者 染为黑色,用新增黑色补足被删除的黑高度。
更复杂的情况是:被摘除节点 及其接替者 均为黑色,此时对应路径的黑高度下降一阶。在 B-树模型中,这对应内部节点下溢。由于路径黑高度出现缺口,该状态称为双黑。
双黑修复的目标,是从兄弟节点或父节点重新分配黑高度。算法关注接替者 (即使为 NULL,也视为具有颜色语义的外部子树)、父节点 、兄弟节点 。根据 及其孩子的颜色分布,删除修复分为四类。

双黑不是一种真实的节点颜色,而是一个“这条路径少了一个黑高度”的记账标记。删除修复的所有分支,都可以理解为三种动作之一:从兄弟那里借一个黑高度、把兄弟和父节点合并后把亏空向上推,或者先旋转把红兄弟转换成黑兄弟情形再处理。
兄弟为黑且含红儿子 (BB-1)
当兄弟节点 为黑色,且它至少拥有一个红色的孩子节点 时。在四阶 B-树的等价模型中,这代表着 所在的节点发生下溢,但相邻兄弟节点内部仍有多个关键码(由红节点表征)。依据 B-树的法则,此时应当发起关键码借用。
在二叉树表示中,算法通过 3+4 局部重构,将红孩子 、兄弟 以及父节点 重新组织。重构后的局部新根继承原父节点 的颜色,而它的两个孩子全部染为黑色。这两个黑色节点补足了 所在分支此前缺失的黑高度。删除操作在这一轮重构后结束。
兄弟为黑且全黑儿子 (BB-2R & BB-2B)
当兄弟节点 为黑色,但其左右两个孩子均不为红时,说明兄弟节点也无法借出关键码,只能将下溢节点与兄弟节点进行合并。这种合并意味着需要将父节点 降入新的超级节点中。在二叉树的操作中,这等价于将兄弟 染红。
然而,根据父节点 原本颜色的不同,处理方式分为两个分支:
-
BB-2R 分支( 原本为红):这代表 在 B-树中归属于更高层级且仍可提供关键码的超级节点。合并后,将 翻转为黑色,直接在局部补足黑高度缺口。下溢解除,调整完成。
-
BB-2B 分支( 原本为黑):这表示 在 B-树中原本就是单关键码节点。节点 转红可以修复底层合并后的局部结构,但会把下溢向上传递。在代码中, 被视为新的双黑位置 ,系统需要继续尾递归(或迭代)处理,最坏情况下需要 步。
兄弟为红 (BB-3)
最后一种情况是兄弟节点 为红色。由红黑树规则可知, 必为黑, 的孩子必全为黑。此时系统围绕 执行一次单旋,并交换颜色: 染黑, 染红。
旋转后,目标 获得一个新的黑色兄弟 。尽管此时包含 的分支仍然处于双黑状态,但由于 已经染成红色,下一步处理不可能进入 BB-2B,只会进入 BB-1(兄弟有红子)或 BB-2R(兄弟无红子且父为红)这两类可在常数时间内结束的分支。该转换保证了全树性质最终恢复。
删除平衡
下面的代码展示了这四类情况的处理逻辑:
1 | // 删除关键码 e,并在需要时修复双黑缺陷。 |
复杂度指标
节点摘除后的修复成本可归纳如下:
| 双黑缺陷状态判定 | 涉及旋转最大频次 | 重染色最大频次 | 修复结果 |
|---|---|---|---|
| 黑兄弟有红子 (BB-1) | 1~2 次 | 3 次 | 局部重构后调整完成。 |
| 黑兄弟无红子且父红 (BB-2R) | 0 次 | 2 次 | 通过重染色完成调整。 |
| 黑兄弟无红子且父黑 (BB-2B) | 0 次 | 1 次 | 必定引发递归,双黑向上攀升,至多波及 层。 |
| 兄弟为红 (BB-3) | 1 次 | 2 次 | 先转换为黑兄弟情形,后续流程必然进入 (BB-1) 或 (BB-2R)。 |
无论删除触发何种局部失衡,红黑树删除修复中的旋转次数最多不超过 3 次:一次来自 BB-3 转换,随后衔接 BB-1 时至多两次局部重构。主要维护开销集中在节点重染色阶段,因此红黑树可以用较小的常数旋转代价维持 的最坏情况复杂度。




