红黑树(Red-Black Tree)

弱平衡二叉搜索树的标准实现 —— 从性质到代码的完整剖析

一、概述

红黑树(Red-Black Tree) 是一种自平衡的二叉搜索树(BST),由 Rudolf Bayer 于1972年发明(当时称为"对称二叉B树"),后在1978年由 Leonidas J. Guibas 和 Robert Sedgewick 完善为现代红黑树形式。每个节点上增加一个存储位表示节点的颜色(红色黑色),通过对任何一条从根到叶子的路径上各节点着色方式的限制,红黑树确保没有一条路径会比其他路径长出两倍,因而接近平衡。

红黑树的核心价值在于:在最坏情况下,查找、插入、删除操作的时间复杂度均为 O(log n)。这一性能保证使得红黑树成为工业界使用最广泛的平衡 BST 实现之一,被大量应用于标准库和操作系统内核中。

红黑树的应用场景

  • Java TreeMap / TreeSet: 底层使用红黑树实现有序映射和有序集合
  • C++ STL map / set: 标准库的有序关联容器基于红黑树
  • Linux 内核: 完全公平调度器(CFS)、虚拟内存管理(VMA)等核心子系统使用红黑树
  • Nginx: 使用红黑树管理定时器事件

术语说明

本文中的"NIL 节点"指的是叶子节点的空指针,在红黑树实现中通常用一个哨兵节点表示。所有 NIL 节点被视为黑色。在实际编码中,通常将空指针统一视为黑色来处理。

二、红黑树的5条性质

红黑树的平衡性由以下 5 条性质 共同保证。理解这些性质是掌握红黑树一切操作的关键前提。

红黑树五大性质

  1. 每个节点要么是红色,要么是黑色。(节点颜色是红黑树的核心状态)
  2. 根节点是黑色的。(保证树有一个稳定的黑色起点)
  3. 所有叶子节点(NIL)都是黑色的。(统一空节点的颜色约定)
  4. 红色节点的两个子节点必须都是黑色的。(不能出现连续的红色节点,这一性质限制了红色节点的分布)
  5. 从任意节点到其每个叶子节点的所有路径都包含相同数量的黑色节点。(这一性质称为"黑高相同",是保证平衡性的核心约束)

黑高(Black-height)概念

节点 x 的黑高 bh(x) 定义为从 x 到其任意叶子节点的路径上黑色节点的数量(不包含 x 自身)。红黑树的第五条规定了所有路径的黑高必须相等。通过这一约束,可以严格证明:一棵有 n 个内部节点的红黑树,其高度最多为 2 log₂(n+1)

从性质4和性质5可以推导出红黑树的一个重要特征:最短路径全是黑色节点,最长路径是红黑节点交替出现,因此最长路径不会超过最短路径的两倍。这正是"弱平衡"的数学本质——它不追求 AVL 树那种严格的左右子树高度差不超过1,转而使用颜色约束来保证近似平衡。

三、红黑树与2-3-4树的等价关系

理解红黑树最直观的方式之一,是将它与2-3-4树(也称4阶B树)对应起来。红黑树本质上是用二叉树的形式"编码"了一棵2-3-4树。

节点对应关系

等价映射的核心思想

在2-3-4树中,一个内部节点可以包含1到3个键。当我们将它"展平"为二叉树时,需要用红色节点来表示那些"和父节点共享同一个2-3-4树节点"的子节点。红色节点总是与它的黑色父节点"捆绑"在一起,共同表示2-3-4树中的一个节点。

这也解释了为什么红黑树的第4条性质规定"红色节点的子节点必须是黑色"——因为红色子节点本身代表了一个2-3-4树节点中的一部分,如果红色节点再连接红色子节点,那就相当于在2-3-4树中出现了超过4个键(即4个以上的子节点),这在4阶B树中是不允许的。

这种等价关系极大地简化了红黑树的插入和删除理解:任何在红黑树中需要调整的情况,都可以视为对应的2-3-4树节点发生了"上溢"(overflow)或"下溢"(underflow),需要拆分或合并节点。

四、插入操作与调整(3种情况)

红黑树的插入分为两步:第一步,按照普通 BST 的规则插入新节点,并将新节点着色为红色(这样做不会破坏性质5,只可能破坏性质4)。第二步,如果破坏了性质4(出现了连续的红色节点),则需要进行调整(Fix-up)

插入新节点 x 后,如果 x 的父节点 p 是黑色,则一切正常。如果 p 也是红色,就出现了连续红色节点的违规。根据 x 的叔父节点(父节点的兄弟节点)的颜色,分为以下三种情况处理。

情况1:叔父节点是红色

操作: 将父节点和叔父节点设为黑色,将祖父节点设为红色,然后将当前节点上移到祖父节点位置继续检查。

情况1(叔父为红):颜色上移 B(黑) B(红) / \ → / \ p(红) u(红) p(黑) u(黑) / x(红)

原理: 这相当于2-3-4树中的一个4-节点发生了上溢(overflow),需要将中间键向上传递到父节点。将祖父节点由黑变红,就是将中间键"向上合并"到父节点中。由于祖父节点变成了红色,如果它的父节点也是红色,则需要继续向上调整。

情况2:叔父节点是黑色,且 x、p、g 呈"三角形"关系(LR 或 RL)

操作: 对父节点 p 进行一次旋转(左旋或右旋),使之变成"直线"形状,从而转化为情况3。旋转后,原来的 x 和 p 角色互换,但颜色不变。

情况2(叔父为黑,三角形):先旋转成直线 g(黑) g(黑) / \ → / \ p(红) u(黑) x(红) u(黑) \ / x(红) p(红) (LR情形,对p左旋) (转化为LL情形)

原理: 这相当于2-3-4树中3-节点的子节点结构需要调整方向。通过一次旋转将"弯曲"的父子关系拉直,为进一步的旋转和变色做准备。

情况3:叔父节点是黑色,且 x、p、g 呈"直线"关系(LL 或 RR)

操作: 对祖父节点 g 进行旋转(右旋 LL 情形,左旋 RR 情形),然后交换原父节点 p 和祖父节点 g 的颜色——p 变成黑色,g 变成红色。

情况3(叔父为黑,直线):旋转 + 变色 g(黑) p(黑) / \ → / \ p(红) u(黑) x(红) g(红) / \ x(红) u(黑) (LL情形,对g右旋) (调整完成,整棵子树平衡)

原理: 这对应2-3-4树中一个4-节点的拆分操作。旋转将子树结构调整为平衡形态,变色则确保红黑性质得以保持。经过情况3的处理后,调整工作到此结束,因为新的子树根节点(原 p)现在是黑色,不会再与上层产生冲突。

插入总结

  • 情况3是"终结情况"——经过一次旋转+变色后调整结束
  • 情况1可能向上传递,最多需要 O(log n) 次
  • 每插入一个节点,最多需要 2次旋转(情况2转情况3,加上情况3的一次旋转)
  • 插入操作总时间复杂度 O(log n)

左旋与右旋的代码实现

旋转是红黑树调整的基础操作。左旋和右旋是一对互逆操作,它们在不破坏 BST 性质的前提下改变树的结构。

左旋操作(Left Rotate) # 对节点x进行左旋:x的右子节点y成为x的父节点 def left_rotate(T, x): y = x.right # y是x的右子节点 x.right = y.left # 将y的左子树变为x的右子树 if y.left != T.nil: y.left.parent = x y.parent = x.parent # y继承x的父节点 if x.parent == T.nil: # x是根节点 T.root = y elif x == x.parent.left: x.parent.left = y else: x.parent.right = y y.left = x # x变为y的左子节点 x.parent = y
右旋操作(Right Rotate) # 对节点y进行右旋:y的左子节点x成为y的父节点 def right_rotate(T, y): x = y.left # x是y的左子节点 y.left = x.right # 将x的右子树变为y的左子树 if x.right != T.nil: x.right.parent = y x.parent = y.parent # x继承y的父节点 if y.parent == T.nil: # y是根节点 T.root = x elif y == y.parent.left: y.parent.left = x else: y.parent.right = x x.right = y # y变为x的右子节点 y.parent = x

插入主流程

红黑树插入 def rb_insert(T, z): # 1. 标准的BST插入 y = T.nil x = T.root while x != T.nil: y = x if z.key < x.key: x = x.left else: x = x.right z.parent = y if y == T.nil: T.root = z elif z.key < y.key: y.left = z else: y.right = z # 2. 初始化新节点为红色 z.left = T.nil z.right = T.nil z.color = RED # 3. 调整修复红黑性质 rb_insert_fixup(T, z) def rb_insert_fixup(T, z): while z.parent.color == RED: # 违反性质4 if z.parent == z.parent.parent.left: uncle = z.parent.parent.right if uncle.color == RED: # 情况1:叔父为红 z.parent.color = BLACK uncle.color = BLACK z.parent.parent.color = RED z = z.parent.parent # 向上传递 else: if z == z.parent.right: # 情况2:三角形(LR) z = z.parent left_rotate(T, z) # 转化为情况3 z.parent.color = BLACK # 情况3:直线(LL) z.parent.parent.color = RED right_rotate(T, z.parent.parent) else: # 对称情形:父节点是右子节点 uncle = z.parent.parent.left if uncle.color == RED: # 对称情况1 z.parent.color = BLACK uncle.color = BLACK z.parent.parent.color = RED z = z.parent.parent else: if z == z.parent.left: # 对称情况2(RL) z = z.parent right_rotate(T, z) z.parent.color = BLACK # 对称情况3(RR) z.parent.parent.color = RED left_rotate(T, z.parent.parent) T.root.color = BLACK # 保证性质2

五、删除操作与调整(4种情况)

红黑树的删除比插入复杂得多。总体思路是:先按 BST 的删除逻辑删除节点,如果被删除的是红色节点,则树的性质没有被破坏;如果被删除的是黑色节点,则违反了性质5(黑高被破坏),需要进行修复。

删除的关键

删除操作的核心难点在于:当删除的节点是黑色时,会导致某些路径上的黑色节点数减少1,破坏了"黑高相等"的性质。修复的本质是向兄弟子树"借"一个黑色节点,或者通过变色和旋转将"额外黑色"向上传递,直到遇到红色节点将其染黑即可终止。

设被删除节点为 x(严格来说,是实际被删除的节点——如果被删除节点有两个孩子,则取其前驱或后继的值覆盖后,删除前驱/后继节点),x 的兄弟节点为 w。删除修复分以下4种情况:

情况1:兄弟节点 w 是红色

操作: 将 w 设为黑色,将 x 的父节点设为红色,然后对父节点进行旋转(x 是左孩子则左旋,是右孩子则右旋)。这样就把红色兄弟变成了黑色侄子的情况,转化为情况2、3或4处理。

目的: 将"兄弟是红色"转化为"兄弟是黑色"的子问题,因为后续所有修复逻辑都假设兄弟是黑色的。

情况2:兄弟节点 w 是黑色,且 w 的两个子节点都是黑色

操作: 将 w 设为红色,然后将 x 上移到父节点位置继续循环。

原理: 这个操作相当于将"额外黑色"向上传递。w 由黑变红等价于从 w 子树中"扣除"了一个黑色高度,使得 w 子树和 x 子树在"缺少一个黑色"上达成一致。

情况3:兄弟节点 w 是黑色,w 的远侧子节点是黑色,近侧子节点是红色

操作: 将 w 的近侧子节点染黑,w 染红,然后对 w 进行旋转(x 是左孩子则右旋 w,反之左旋 w),转化为情况4。

目的: 为情况4创造条件,让"远侧子节点"变成红色。

情况4:兄弟节点 w 是黑色,且 w 的远侧子节点是红色

操作: 将父节点的颜色赋给 w,父节点染黑,w 的远侧子节点染黑,对父节点进行旋转(x 是左孩子则左旋,反之右旋)。这是删除修复的终结情况。

原理: 通过这部旋转+变色操作,在 x 子树中"补充"了一个黑色节点(从父节点和兄弟子树中"借来"的),同时保持了兄弟子树的黑高不变。

删除总结

  • 情况4是"终结情况"——处理完成后循环立即结束
  • 情况1经过一次旋转转换为情况2/3/4
  • 情况2向上传递,最多 O(log n) 次
  • 情况3转换为情况4
  • 删除操作最多需要 3次旋转
  • 删除操作总时间复杂度 O(log n)
删除修复(伪代码) def rb_delete_fixup(T, x): while x != T.root and x.color == BLACK: if x == x.parent.left: w = x.parent.right # 兄弟节点 if w.color == RED: # 情况1:兄弟为红 w.color = BLACK x.parent.color = RED left_rotate(T, x.parent) w = x.parent.right # 更新兄弟,转为2/3/4 if w.left.color == BLACK and w.right.color == BLACK: w.color = RED # 情况2:兄弟和它的子节点都是黑 x = x.parent # 向上传递 else: if w.right.color == BLACK: # 情况3:远侧子为黑 w.left.color = BLACK w.color = RED right_rotate(T, w) w = x.parent.right w.color = x.parent.color # 情况4:远侧子为红 x.parent.color = BLACK w.right.color = BLACK left_rotate(T, x.parent) x = T.root # 终止循环 else: # 对称情形 # 对称处理... x.color = BLACK

六、时间复杂度分析

红黑树的高度保证

定理: 一棵有 n 个内部节点的红黑树,其高度 h ≤ 2 log₂(n+1)。

证明概要:

  1. 设 bh(root) 为根节点的黑高。由于性质4(不能有连续红色节点),从根到叶子的路径上红色节点不超过黑色节点,因此 h ≤ 2 · bh(root)。
  2. 对一棵黑高为 bh 的红黑树,其最少内部节点数为 2^bh - 1(通过归纳法证明:黑高为 bh 的子树至少包含一棵完全黑色满二叉树)。
  3. 因此 n ≥ 2^bh(root) - 1,即 bh(root) ≤ log₂(n+1)。
  4. 结合 h ≤ 2 · bh(root),得到 h ≤ 2 log₂(n+1)。
操作 平均时间复杂度 最坏时间复杂度 旋转次数
查找(Search) O(log n) O(log n) 0
插入(Insert) O(log n) O(log n) ≤ 2
删除(Delete) O(log n) O(log n) ≤ 3
最小值(Minimum) O(log n) O(log n) 0
最大值(Maximum) O(log n) O(log n) 0
前驱/后继(Predecessor/Successor) O(log n) O(log n) 0

红黑树的实际查找性能通常略差于 AVL 树(因为树高可能更大一些),但插入和删除的旋转次数显著更少(AVL 树在插入/删除时可能需要 O(log n) 次旋转)。这一特性使红黑树在写操作频繁的场景中表现更优

七、在 TreeMap / TreeSet 中的应用

Java 的 java.util.TreeMapTreeSet 是红黑树最著名的工业应用之一。TreeSet 底层实际上是一个 TreeMap(键为元素,值为一个常量对象),因此二者的核心都是红黑树。

TreeMap 的红黑树实现特点

Java TreeMap 的使用示例 import java.util.*; public class TreeMapExample { public static void main(String[] args) { // 红黑树底层实现的有序Map TreeMap<Integer, String> map = new TreeMap<>(); map.put(50, "A"); map.put(30, "B"); map.put(80, "C"); map.put(20, "D"); map.put(40, "E"); // 中序遍历 = 按键升序遍历 for (Map.Entry<Integer, String> e : map.entrySet()) { System.out.println(e.getKey() + ": " + e.getValue()); } // 输出:20:D, 30:B, 40:E, 50:A, 80:C // 有序操作 System.out.println(map.firstKey()); // 20 System.out.println(map.lastKey()); // 80 System.out.println(map.lowerKey(35)); // 30(小于35的最大键) System.out.println(map.higherKey(35)); // 40(大于35的最小键) System.out.println(map.ceilingKey(35)); // 40(大于等于35的最小键) // 子范围视图 SortedMap<Integer, String> sub = map.subMap(20, 50); // 包含 20, 30, 40 } }

TreeSet 的使用方式类似,但只存储键。两者都保证所有操作的时间复杂度为 O(log n),是 Java 集合框架中不可或缺的有序数据结构。

八、红黑树 vs AVL 树

红黑树和 AVL 树都是自平衡 BST 的经典实现,但它们的设计哲学不同,适用于不同的场景。

对比维度 红黑树 AVL 树
平衡标准 最长路径 ≤ 2 × 最短路径(弱平衡) 左右子树高度差 ≤ 1(严格平衡)
树高 ≤ 2 log₂(n+1) ≤ 1.44 log₂(n+1)
查找性能 O(log n),但常数稍大 O(log n),树更矮,查找稍快
插入旋转次数 ≤ 2 次 ≤ 2 次
删除旋转次数 ≤ 3 次 O(log n) 次(可能需要向上回溯到根)
额外存储 1 bit(颜色) ≤ 2 bits(平衡因子 -1, 0, +1)
实现复杂度 较复杂,尤其是删除 相对简单
典型应用 Java TreeMap, C++ map, Linux 内核 数据库索引(读多写少的场景)

选型建议

  • 读多写少 → 优先考虑 AVL 树。更严格的平衡带来更快的查找速度,而插入/删除的旋转开销不是瓶颈。
  • 写操作频繁 → 优先考虑红黑树。红黑树在插入和删除时的旋转次数有严格的常数上限,虽然树略高一些,但总体性能更稳定。
  • 系统底层 / 通用库 → 优先选择红黑树。Java、C++ 标准库和 Linux 内核的选择已经证明了这一点:红黑树在最坏情况下的性能保证更好,更适合作为通用的基础数据结构。

九、Python 简化实现

下面给出一个 Python 实现的简化版红黑树,重点关注插入和查找的实现。删除操作由于篇幅原因仅提供框架性代码。

Python 红黑树简化实现 class RBNode: def __init__(self, key, value=None): self.key = key self.value = value self.left = None self.right = None self.parent = None self.color = RED # 新节点默认红色 RED = 'RED' BLACK = 'BLACK' class RedBlackTree: def __init__(self): self.nil = RBNode(None) # 哨兵NIL节点 self.nil.color = BLACK self.nil.left = None self.nil.right = None self.root = self.nil def left_rotate(self, x): """左旋:以x为轴向左旋转""" y = x.right x.right = y.left if y.left != self.nil: y.left.parent = x y.parent = x.parent if x.parent == self.nil: self.root = y elif x == x.parent.left: x.parent.left = y else: x.parent.right = y y.left = x x.parent = y def right_rotate(self, y): """右旋:以y为轴向右旋转""" x = y.left y.left = x.right if x.right != self.nil: x.right.parent = y x.parent = y.parent if y.parent == self.nil: self.root = x elif y == y.parent.right: y.parent.right = x else: y.parent.left = x x.right = y y.parent = x def insert(self, key, value=None): """插入一个键值对""" z = RBNode(key, value) z.left = self.nil z.right = self.nil z.color = RED # BST插入 y = self.nil x = self.root while x != self.nil: y = x if z.key < x.key: x = x.left else: x = x.right z.parent = y if y == self.nil: self.root = z elif z.key < y.key: y.left = z else: y.right = z self._insert_fixup(z) def _insert_fixup(self, z): """插入后调整红黑性质""" while z.parent.color == RED: if z.parent == z.parent.parent.left: uncle = z.parent.parent.right if uncle.color == RED: # 情况1 z.parent.color = BLACK uncle.color = BLACK z.parent.parent.color = RED z = z.parent.parent else: if z == z.parent.right: # 情况2 → 情况3 z = z.parent self.left_rotate(z) z.parent.color = BLACK # 情况3 z.parent.parent.color = RED self.right_rotate(z.parent.parent) else: uncle = z.parent.parent.left if uncle.color == RED: # 对称情况1 z.parent.color = BLACK uncle.color = BLACK z.parent.parent.color = RED z = z.parent.parent else: if z == z.parent.left: # 对称情况2 → 3 z = z.parent self.right_rotate(z) z.parent.color = BLACK # 对称情况3 z.parent.parent.color = RED self.left_rotate(z.parent.parent) self.root.color = BLACK def search(self, key): """查找指定键的节点""" x = self.root while x != self.nil and key != x.key: if key < x.key: x = x.left else: x = x.right return x if x != self.nil else None def inorder(self): """中序遍历:返回有序的键值对列表""" result = [] self._inorder(self.root, result) return result def _inorder(self, x, result): if x != self.nil: self._inorder(x.left, result) result.append((x.key, x.value)) self._inorder(x.right, result) def minimum(self): """返回最小的键""" x = self.root if x == self.nil: return None while x.left != self.nil: x = x.left return x.key def maximum(self): """返回最大的键""" x = self.root if x == self.nil: return None while x.right != self.nil: x = x.right return x.key
测试验证 # 创建红黑树并插入数据 rbt = RedBlackTree() keys = [41, 38, 31, 12, 19, 8, 50, 45, 60] for k in keys: rbt.insert(k, chr(64 + k)) # 验证中序有序性 result = rbt.inorder() print([k for k, v in result]) # 输出: [8, 12, 19, 31, 38, 41, 45, 50, 60] # 查找测试 node = rbt.search(19) print(f"查找19: key={node.key}, value={node.value}") # 输出: 查找19: key=19, value=S # 最值测试 print(f"最小值: {rbt.minimum()}") # 8 print(f"最大值: {rbt.maximum()}") # 60 # 插入相同键(更新值) rbt.insert(19, "Updated") node = rbt.search(19) print(f"更新后: key={node.key}, value={node.value}") # 输出: 更新后: key=19, value=Updated

十、核心要点总结

十一、进一步思考

红黑树的学习进阶路径

  1. 基础层: 理解5条性质和高度证明,掌握左旋/右旋的代码实现
  2. 理解层: 通过2-3-4树的等价关系,理解插入和删除各情况背后的几何意义
  3. 编码层: 手写红黑树的插入和删除,使用大量随机数据测试验证
  4. 应用层: 阅读 Java TreeMap 源码(java.util.TreeMap),理解工业级实现中的细节处理
  5. 扩展层: 学习左倾红黑树(LLRB)、AA 树等变体,理解不同平衡策略的权衡

面试常见问题

  • 红黑树的5条性质是什么?为什么这些性质能保证 O(log n) 高度?
  • 红黑树插入后有哪些调整情况?画出每种情况的变换图。
  • 红黑树和 AVL 树的区别是什么?在实际工程中如何选择?
  • 为什么红黑树插入最多只需要2次旋转?
  • 红黑树中红色节点不能连续出现,为什么允许黑色节点连续出现?
  • 红黑树是严格平衡的二叉搜索树吗?为什么称其为"弱平衡"?

红黑树左倾变体

Robert Sedgewick 在2008年提出了左倾红黑树(Left-Leaning Red-Black Tree, LLRB),它强制要求所有3-节点都表示为"黑色父节点 + 红色左子节点"的形式。这一约束极大地简化了实现——插入只需要处理"红色右子节点"和"连续红色左子节点"两种情况。LLRB 的代码量约为标准红黑树的一半,是学习红黑树理想的入门实现。但需要注意的是,LLRB 在删除操作中仍然保留了标准红黑树的全部复杂性。

红黑树是计算机科学中"优雅的工程妥协"的典范——它不追求理论上的完美平衡,而是通过精心设计的颜色约束和旋转策略,在平衡性保证操作效率之间取得了完美的平衡点。这种"足够好"的设计哲学使得红黑树成为过去四十年来最成功的数据结构之一。