ESC
输入关键词搜索文章
目录

红黑树

数据结构 · 自平衡 · 对数保证
用红与黑的约束,编码 2-3-4 树的完美平衡
5条核心性质
O(log n)最坏查询
2种旋转
3最多旋转(插入)
动机
平衡之困:二叉搜索树的退化

二叉搜索树(BST)看起来很美:中序遍历有序,查询、插入、删除都是 $O(h)$,其中 $h$ 是树高。理想情况下 $h = O(\log n)$,一切都很高效。

但现实很骨感。考虑按顺序插入 1, 2, 3, 4, 5

graph TD
  1 --> 2
  2 --> 3
  3 --> 4
  4 --> 5
  1 --> N1["NIL"]
  2 --> N2["NIL"]
  3 --> N3["NIL"]
  4 --> N4["NIL"]
  5 --> N5["NIL"]
  5 --> N6["NIL"]
  style 1 fill:#444,color:#fff
  style 2 fill:#444,color:#fff
  style 3 fill:#444,color:#fff
  style 4 fill:#444,color:#fff
  style 5 fill:#444,color:#fff

一条链!退化成了链表,所有操作都变成 $O(n)$。这就是 BST 的平衡问题——它的性能完全取决于输入数据的顺序。

核心矛盾:BST 的效率依赖于树的"胖矮"形态,但插入/删除操作天然倾向于让树变"瘦长"。我们需要一种机制,在每次修改后主动维护树的平衡。

AVL 树是一种方案(严格平衡,左右子树高度差 ≤ 1),但它过于保守——每次插入删除可能触发大量旋转。红黑树走了另一条路:不追求完美平衡,只保证"近似平衡",换取更少的维护代价。

等价
理解红黑树的钥匙:2-3-4 树

很多人学红黑树时被各种 Case 搞得头晕眼花——什么时候左旋、什么时候右旋、什么时候变色?死记硬背痛苦且低效。理解红黑树最好的方式,是认识到一个关键事实:

红黑树等价于 2-3-4 树。红黑树中的每一条"红链"(红色节点到其红色子节点的链接),本质上是在用二叉树的形态"模拟"2-3-4 树中的一个多键节点。掌握了这个映射关系,你不再需要死记旋转规则——只需在 2-3-4 树上操作,然后翻译回红黑树即可。

什么是 2-3-4 树?

2-3-4 树是一种 B 树(阶数 4),它的每个节点可以有 1~3 个键和 2~4 个子节点:

节点类型键数子节点数示例
2-节点12[5]
3-节点23[3, 7]
4-节点34[2, 5, 8]

2-3-4 树是完美平衡的:所有叶子节点都在同一层。这使得它的所有操作都是 $O(\log n)$。但 2-3-4 树的实现很麻烦——需要处理可变长度的键数组和子节点数组。红黑树就是用"着色"技巧把 2-3-4 树编码成二叉树。

节点映射关系

2-3-4 树的三种节点,在红黑树中各有对应的二叉树形态:

2-3-4 树节点红黑树表示规则
2-节点 [k]一个黑色节点 k直接对应
3-节点 [a, b]黑色节点 + 红色子节点(两种排列)红链"粘合"两个键
4-节点 [a, b, c]黑色节点 + 两个红色子节点两条红链粘合三个键

以 3-节点为例:[3, 7] 可以表示为"3(黑)→ 7(红,右子)"或"7(黑)→ 3(红,左子)"——这就是红黑树中红色节点可以"左倾"或"右倾"的来源。左倾红黑树(LLRB)强制红色链接只出现在左侧,使对应关系变为 1:1。

用 2-3-4 树理解红黑树性质

红黑树的性质在 2-3-4 树视角下变得极其自然:

· 性质 3(红节点不能有红子节点)→ 2-3-4 树最多是 4-节点,不可能有 5-节点

· 性质 5(所有路径黑高相同)→ 2-3-4 树所有叶子在同一层,黑节点 = 2-3-4 树的节点

  • 插入时的"重着色" → 对应 4-节点分裂
  • 删除时的"双重黑" → 对应 2-节点下溢
  • 定义
    红黑树的五条性质

    红黑树是一棵带颜色标记的二叉搜索树,每个节点被染成红色黑色。它必须同时满足以下五条性质:

    #性质直觉含义
    1每个节点是红色或黑色颜色是二值的,没有灰色地带
    2根节点是黑色树的起点必须是"稳重的"颜色
    3所有 NIL 叶子(哨兵)是黑色叶子节点统一为黑,这是"底部约束"
    4红色节点的两个子节点必须是黑色不能有连续的红节点(核心限制)
    5从任一节点到其所有后代 NIL 的路径,黑色节点数量相同"黑高"一致,这是平衡的根源

    NIL 哨兵节点

    红黑树中,所有"空指针"都被替换为一个统一的 NIL 哨兵节点NIL),它是黑色的。实际实现中,这个哨兵可以是一个共享的全局对象。引入哨兵的目的是让每条"到叶子的路径"都有明确的终点,简化性质 5 的表述和代码逻辑。

    为什么这五条性质能保证平衡?关键在于性质 4 和性质 5 的配合:

    性质 4 告诉我们:一条路径上不可能连续出现两个红节点,所以红色节点最多只能"穿插"在黑色节点之间。这意味着从根到最远叶子的路径上,红色节点数 ≤ 黑色节点数。

    性质 5 告诉我们:所有路径的黑高(black-height,记为 $bh$)相同。

    合在一起:最长路径 ≤ $2 \times bh$,最短路径 ≥ $bh$。而 $bh \geq h/2$,所以:

    $$n \geq 2^{bh} - 1 \implies bh \leq \log_2(n+1)$$

    因此 $h \leq 2\log_2(n+1) = O(\log n)$五条性质共同锁死了树高的上界

    基础操作
    旋转:红黑树的原子操作

    旋转(Rotation)是红黑树维护平衡的基本操作,所有插入和删除的修复都通过旋转完成。旋转分为左旋右旋,它们互为逆操作。

    左旋(Left Rotate)

    左旋以节点 x 为支点,将其右子节点 y 提升到 x 的位置,x 变为 y 的左子节点。

    graph TD
      subgraph Before["旋转前"]
        A1["x"] --> B1["α"]
        A1 --> C1["y"]
        C1 --> D1["β"]
        C1 --> E1["γ"]
      end
    
    graph TD
      subgraph After["旋转后"]
        A2["y"] --> B2["x"]
        A2 --> C2["γ"]
        B2 --> D2["α"]
        B2 --> E2["β"]
      end
    
    旋转的关键性质:旋转只改变了局部的父子关系,不改变 BST 的中序遍历顺序。它保持了搜索树的有序性,只调整了树的结构形态。旋转操作本身是 $O(1)$ 的。

    右旋(Right Rotate)

    右旋是左旋的镜像操作——以节点 y 为支点,将其左子节点 x 提升到 y 的位置。从 2-3-4 树的视角来看,左旋和右旋都是在重新分配同一个多键节点内部的键的排列方式。

    插入
    插入与修复:从 2-3-4 树看颜色传播

    红黑树的插入分为两步:标准 BST 插入 + 颜色修复

    Step 1:BST 插入

    和普通 BST 一样,找到新节点应该插入的位置。新节点默认染成红色

    为什么新节点染红?

    如果染黑,插入后必然违反性质 5(黑高变化)。染红只可能违反性质 4(父节点也是红色),而性质 4 的修复相对容易,是局部操作。从 2-3-4 树的角度看:插入红节点 = 向 2-3-4 树的一个节点中添加一个键,这可能导致节点"溢出"(overflow),需要分裂。

    Step 2:修复 Insert Fixup

    插入后,如果新节点 z 的父节点是红色,就违反了性质 4(对应 2-3-4 树中出现了 4-节点溢出)。修复的核心思想是:通过重新着色和旋转,把"溢出"向上传播,直到消解

    z 的父节点为 p,叔节点为 u,祖父节点为 g。根据 u 的颜色和 z 的位置,分三种情况:

    Case条件2-3-4 树视角红黑树操作
    1u4-节点分裂:中间键上移pu 染黑,g 染红;z ← g
    2u 黑,zp 的"弯"子3-节点内部调整方向旋转 p,转化为 Case 3
    3u 黑,zp 的"直"子3-节点提升p 染黑,g 染红;旋转 g

    用流程图来直观展示修复的决策路径:

    graph TD
      A["z 的父节点 p 是红色?"] -->|"否"| DONE["无需修复"]
      A -->|"是"| B{"叔节点 u 的颜色?"}
      B -->|"红色"| C["Case 1: 4-节点分裂
    p、u 染黑,g 染红
    z ← g,继续循环"] B -->|"黑色"| D{"z 和 p 方向一致?"} D -->|"不一致(弯折)"| E["Case 2: 旋转对齐方向
    转化为 Case 3"] D -->|"一致(直线)"| F["Case 3: p 染黑,g 染红
    旋转 g,修复完成"] E --> F C --> A style C fill:#e74c3c,color:#fff style E fill:#f39c12,color:#fff style F fill:#27ae60,color:#fff

    2-3-4 树视角详解

    Case 1:叔节点是红色(4-节点分裂)

    p 和叔 u 都是红色,意味着祖父 gpu 构成了一个 4-节点(三个键粘合在一起)。现在又插入了一个红节点 z,形成 5-节点——溢出了!解决方式是把中间键(g)上移,左右两个键各自独立:pu 变成黑色(独立节点),g 变红色(上移)。然后以 g 为新的 z 继续检查——因为 g 变红后可能又和它的父节点冲突。

    Case 2 → Case 3:方向对齐(3-节点内部调整)

    u 是黑色,说明 2-3-4 树中 zp 构成的 3-节点要和 g 合并。但如果 zp 的方向不一致(比如 pg 的左子,但 zp 的右子),就需要先旋转对齐——在 2-3-4 树视角下,这只是在重新排列 3-节点内部键的顺序。

    Case 3:终极修复(3-节点提升)

    方向对齐后,一次旋转就能让 p 提升为新的子树根,g 降级为 p 的子节点。颜色交换后,性质 4 和性质 5 同时恢复。

    复杂度分析

    插入修复中:

    · Case 1 会让 z 上移两层(到祖父),最多循环 $O(\log n)$ 次。

    · Case 2 一次旋转就转化为 Case 3。

    · Case 3 一次旋转就彻底结束。

    因此,插入修复最多 $O(\log n)$ 次重着色,最多 2 次旋转。总时间 $O(\log n)$

    删除
    删除与修复:红黑树最复杂的操作

    删除是红黑树中最复杂的操作。理解它的关键是抓住一条线索:删除一个节点后,什么性质可能被破坏?

    Step 1:BST 删除

    和普通 BST 类似,先定位待删除节点 z,根据子节点数分三种情况:

    z 的子节点数操作实际被移除的节点
    无子节点(叶子)直接删除z 本身
    一个子节点用子节点替代 zz 本身
    两个子节点找到中序后继 y,用 y 替代 z后继 y(至多有一个右子)

    统一模型:记实际被移除的节点为 y,替代 y 位置的节点为 x。我们需要关注的是 y 的颜色:

    · 如果 y红色,删除后不违反任何性质。无需修复。

    · 如果 y黑色,删除可能导致性质 4 或 5 被破坏,需要调用 delete_fixup(x)

    黑色节点被删除的后果

    删除黑色节点 y 会带来两个问题:

    1. 性质 5:经过 y 的所有路径少了一个黑节点,黑高不一致(对应 2-3-4 树中的下溢)。

    2. 性质 4:如果 y 的替代者 xy 的父节点都是红色,会出现连续红节点。

    修复的核心思路是给 x 额外"加一层黑色"(称为"双重黑"),然后通过旋转和重着色将这层多余的黑色消化掉。

    Step 2:修复 Delete Fixup

    删除修复处理节点 x(它带有"额外的黑色")。设 wx 的兄弟节点。修复分为四种情况:

    Case条件2-3-4 树视角操作
    1w 是红色兄弟是 3-节点,先"旋转展开"w 染黑,p 染红;旋转,进入 2/3/4
    2w 黑,w 两个子都黑兄弟是 2-节点,借不到,合并w 染红,x ← x.parent
    3w 黑,近侄子红、远侄子黑兄弟是 3-节点,先调整方向调整颜色,旋转 w,转化为 Case 4
    4w 黑,远侄子红兄弟有富余键,借键交换颜色,旋转 p,结束
    graph TD
      START["x 带有额外黑色"] --> A{"兄弟 w 的颜色?"}
      A -->|"红色"| B["Case 1: 展开兄弟
    w 染黑,p 染红
    旋转,w ← 新兄弟"] A -->|"黑色"| C{"w 的子节点颜色?"} C -->|"两个都黑"| D["Case 2: 合并
    w 染红
    x ← x.parent,继续"] C -->|"远侄子黑,近侄子红"| E["Case 3: 调整方向
    转化为 Case 4"] C -->|"远侄子红"| F["Case 4: 借键
    交换颜色,旋转 p → 结束"] B --> C E --> F style B fill:#e74c3c,color:#fff style D fill:#f39c12,color:#fff style E fill:#3498db,color:#fff style F fill:#27ae60,color:#fff

    2-3-4 树视角详解

    从 2-3-4 树的角度理解删除修复,整个逻辑变得非常清晰:删除黑色节点 = 2-3-4 树中从 2-节点删除键,导致该节点变空(下溢)。此时有两种策略:

    · 借键(Case 3/4):如果兄弟节点有富余键(是 3-节点或 4-节点),就向它借一个。借键的过程是:父节点的一个键下移到被删节点,兄弟节点的一个键上移到父节点。Case 4 就是借键的最终形态。

    · 合并(Case 2):如果兄弟也是 2-节点(没有富余),那只能"合并"——把父节点的一个键拉下来和兄弟合并成 3-节点。但这会让父节点少一个键,可能引发父节点的下溢——所以需要以父节点为新的 x 继续向上修复。

    Case 1 只是一个"预处理"步骤,把红色兄弟旋转成黑色兄弟,让后续的借键/合并操作可以正确执行。

    复杂度分析

    删除修复中:

    · Case 1 一次旋转后转化为 Case 2/3/4。

    · Case 2x 上移一层,最多循环 $O(\log n)$ 次。

    · Case 3 一次旋转后转化为 Case 4。

    · Case 4 一次旋转后结束。

    因此,删除修复最多 $O(\log n)$ 次重着色,最多 3 次旋转。总时间 $O(\log n)$

    全景
    操作复杂度总览
    操作时间复杂度旋转次数重着色次数
    查询(Search)$O(\log n)$00
    前驱/后继(Min/Max)$O(\log n)$00
    插入(Insert)$O(\log n)$≤ 2$O(\log n)$
    删除(Delete)$O(\log n)$≤ 3$O(\log n)$

    对比 AVL 树:

    特性红黑树AVL 树
    平衡标准宽松:最长路径 ≤ 2× 最短路径严格:左右子树高度差 ≤ 1
    查询性能$O(\log n)$,常数因子略大$O(\log n)$,常数因子更小
    插入旋转≤ 2 次$O(\log n)$
    删除旋转≤ 3 次$O(\log n)$
    适用场景写多读少(如标准库 map读多写少(如数据库索引)
    实现
    Python 完整实现

    以下是一个完整的红黑树 Python 实现,包含插入、删除、搜索和可视化输出。每个方法都带有详细注释,与上文讲解的 Case 一一对应。

    查看完整 Python 代码
    """
    Red-Black Tree — Python Implementation
    =======================================
    基于 CLRS《算法导论》第 13 章的标准实现。
    """
    
    RED = True
    BLACK = False
    
    
    class NilNode:
        """NIL 哨兵节点:所有叶子指针统一指向此对象"""
        def __init__(self):
            self.color = BLACK
            self.left = self.right = self.parent = None
    
    
    NIL = NilNode()
    
    
    class Node:
        """红黑树节点"""
        __slots__ = ('key', 'color', 'left', 'right', 'parent')
    
        def __init__(self, key, color=RED,
                     left=NIL, right=NIL, parent=NIL):
            self.key = key
            self.color = color
            self.left = left
            self.right = right
            self.parent = parent
    
        def __repr__(self):
            c = 'R' if self.color == RED else 'B'
            return f"{self.key}({c})"
    
    
    class RedBlackTree:
        """红黑树:支持 insert / delete / search / inorder"""
    
        def __init__(self):
            self.root = NIL
    
        # ── 搜索 ─────────────────────────────────────
        def search(self, key):
            """标准 BST 搜索,O(log n)"""
            x = self.root
            while x is not NIL:
                if key == x.key:
                    return x
                x = x.left if key < x.key else x.right
            return None
    
        def minimum(self, x=None):
            """子树最小节点"""
            if x is None:
                x = self.root
            while x.left is not NIL:
                x = x.left
            return x
    
        # ── 旋转 ─────────────────────────────────────
        def _left_rotate(self, x):
            """
            左旋:以 x 为支点
                  x                y
                 / \     →       / \
                α   y           x   γ
                   / \         / \
                  β   γ       α   β
            """
            y = x.right
            x.right = y.left          # β 挂到 x 的右子
            if y.left is not NIL:
                y.left.parent = x
    
            y.parent = x.parent       # y 接管 x 的位置
            if x.parent is NIL:
                self.root = y
            elif x is x.parent.left:
                x.parent.left = y
            else:
                x.parent.right = y
    
            y.left = x                # x 变为 y 的左子
            x.parent = y
    
        def _right_rotate(self, y):
            """右旋:左旋的镜像"""
            x = y.left
            y.left = x.right
            if x.right is not NIL:
                x.right.parent = y
    
            x.parent = y.parent
            if y.parent is NIL:
                self.root = x
            elif y is y.parent.right:
                y.parent.right = x
            else:
                y.parent.left = x
    
            x.right = y
            y.parent = x
    
        # ── 插入 ─────────────────────────────────────
        def insert(self, key):
            """插入 key,O(log n)"""
            z = Node(key)
    
            # 标准 BST 插入
            y = NIL
            x = self.root
            while x is not NIL:
                y = x
                x = x.left if z.key < x.key else x.right
    
            z.parent = y
            if y is NIL:
                self.root = z
            elif z.key < y.key:
                y.left = z
            else:
                y.right = z
    
            # 新节点默认红色,可能违反性质 4
            self._insert_fixup(z)
    
        def _insert_fixup(self, z):
            """
            插入修复 — 3 种 Case
            从 2-3-4 树视角:处理 4-节点溢出
            """
            while z.parent.color == RED:
                if z.parent is z.parent.parent.left:
                    # 父节点是祖父的左子
                    u = z.parent.parent.right    # 叔节点
    
                    if u.color == RED:
                        # ── Case 1: 叔红 → 4-节点分裂 ──
                        z.parent.color = BLACK
                        u.color = BLACK
                        z.parent.parent.color = RED
                        z = z.parent.parent       # 上移两层
    
                    else:
                        if z is z.parent.right:
                            # ── Case 2: z 是"弯"子 → 对齐方向 ──
                            z = z.parent
                            self._left_rotate(z)
    
                        # ── Case 3: z 是"直"子 → 一次旋转搞定 ──
                        z.parent.color = BLACK
                        z.parent.parent.color = RED
                        self._right_rotate(z.parent.parent)
    
                else:
                    # 镜像:父节点是祖父的右子
                    u = z.parent.parent.left
    
                    if u.color == RED:
                        z.parent.color = BLACK
                        u.color = BLACK
                        z.parent.parent.color = RED
                        z = z.parent.parent
                    else:
                        if z is z.parent.left:
                            z = z.parent
                            self._right_rotate(z)
                        z.parent.color = BLACK
                        z.parent.parent.color = RED
                        self._left_rotate(z.parent.parent)
    
            self.root.color = BLACK    # 确保根是黑色
    
        # ── 删除 ─────────────────────────────────────
        def delete(self, key):
            """删除 key,O(log n)"""
            z = self.search(key)
            if z is None:
                return
    
            y = z
            y_original_color = y.color
    
            if z.left is NIL:
                x = z.right
                self._transplant(z, z.right)
            elif z.right is NIL:
                x = z.left
                self._transplant(z, z.left)
            else:
                # 两个子节点:找中序后继
                y = self.minimum(z.right)
                y_original_color = y.color
                x = y.right
    
                if y.parent is z:
                    x.parent = y       # 处理 x == NIL
                else:
                    self._transplant(y, y.right)
                    y.right = z.right
                    y.right.parent = y
    
                self._transplant(z, y)
                y.left = z.left
                y.left.parent = y
                y.color = z.color
    
            if y_original_color == BLACK:
                self._delete_fixup(x)
    
        def _transplant(self, u, v):
            """用子树 v 替换子树 u"""
            if u.parent is NIL:
                self.root = v
            elif u is u.parent.left:
                u.parent.left = v
            else:
                u.parent.right = v
            v.parent = u.parent
    
        def _delete_fixup(self, x):
            """
            删除修复 — 4 种 Case
            从 2-3-4 树视角:处理 2-节点下溢(借键/合并)
            """
            while x is not self.root and x.color == BLACK:
                if x is x.parent.left:
                    w = x.parent.right   # 兄弟
    
                    if w.color == RED:
                        # ── Case 1: 兄弟红 → 展开为黑兄弟 ──
                        w.color = BLACK
                        x.parent.color = RED
                        self._left_rotate(x.parent)
                        w = x.parent.right
    
                    if (w.left.color == BLACK and
                            w.right.color == BLACK):
                        # ── Case 2: 兄弟两子都黑 → 合并 ──
                        w.color = RED
                        x = x.parent
    
                    else:
                        if w.right.color == BLACK:
                            # ── Case 3: 远侄子黑 → 调整方向 ──
                            w.left.color = BLACK
                            w.color = RED
                            self._right_rotate(w)
                            w = x.parent.right
    
                        # ── Case 4: 远侄子红 → 借键,修复完成 ──
                        w.color = x.parent.color
                        x.parent.color = BLACK
                        w.right.color = BLACK
                        self._left_rotate(x.parent)
                        x = self.root       # 终止循环
    
                else:
                    # 镜像
                    w = x.parent.left
                    if w.color == RED:
                        w.color = BLACK
                        x.parent.color = RED
                        self._right_rotate(x.parent)
                        w = x.parent.left
    
                    if (w.right.color == BLACK and
                            w.left.color == BLACK):
                        w.color = RED
                        x = x.parent
                    else:
                        if w.left.color == BLACK:
                            w.right.color = BLACK
                            w.color = RED
                            self._left_rotate(w)
                            w = x.parent.left
    
                        w.color = x.parent.color
                        x.parent.color = BLACK
                        w.left.color = BLACK
                        self._right_rotate(x.parent)
                        x = self.root
    
            x.color = BLACK
    
        # ── 遍历 ─────────────────────────────────────
        def inorder(self):
            """中序遍历,返回有序列表"""
            result = []
            def _walk(x):
                if x is NIL:
                    return
                _walk(x.left)
                c = 'R' if x.color == RED else 'B'
                result.append(f"{x.key}({c})")
                _walk(x.right)
            _walk(self.root)
            return result
    
    
    # ── 使用示例 ─────────────────────────────────────
    if __name__ == "__main__":
        rbt = RedBlackTree()
    
        # 插入
        for key in [7, 3, 18, 10, 22, 8, 11, 26]:
            rbt.insert(key)
    
        print("插入后中序:", " → ".join(rbt.inorder()))
        # 输出: 3(B) → 7(R) → 8(R) → 10(B) → 11(R) → 18(B) → 22(B) → 26(R)
    
        # 搜索
        node = rbt.search(10)
        print(f"搜索 10: {node}")   # 10(B)
    
        # 删除
        rbt.delete(18)
        print("删除 18 后:", " → ".join(rbt.inorder()))
      

    这个实现的关键设计决策:

    • NilNode 哨兵:用全局共享的 NIL 对象替代 None,简化边界条件判断
    • 颜色用布尔值RED = True, BLACK = False,比枚举更轻量
    • insert_fixup 和 delete_fixup:与上文讲解的 Case 一一对应,注释标注了每个分支对应的 Case
    应用
    红黑树在真实系统中的应用

    红黑树不是教科书中的纸上谈兵——它是工程实践中被使用最广泛的平衡搜索树之一。选择红黑树而非 AVL 树的核心原因是:红黑树的旋转次数有常数上界(插入 ≤ 2 次,删除 ≤ 3 次),这使得它在频繁修改的场景下性能更稳定。

    系统应用场景为什么选红黑树?
    Linux 内核完全公平调度器(CFS)、epoll、内存管理需要 $O(\log n)$ 的插入/删除保证,旋转次数有上界
    C++ STLstd::mapstd::setstd::multimap写操作频繁时,红黑树的修改代价低于 AVL
    JavaTreeMapTreeSet与 STL 同理;Java 8 的 HashMap 在哈希冲突严重时也会退化为红黑树
    Python某些第三方库(如 bintrees内置的 dict 用哈希表,但有序映射场景需要红黑树
    函数式编程持久化数据结构(Haskell Data.Map红黑树支持纯函数式实现(Okasaki, 1999),每次修改只影响 $O(\log n)$ 个节点
    计算几何线段扫除法、区间树需要最坏情况 $O(\log n)$ 的查询和修改保证

    Java HashMap 的退化优化

    Java 8 对 HashMap 做了一个精妙的优化:当哈希桶中的链表长度超过阈值(默认 8)时,链表会自动转换为红黑树。这使得最坏情况下的查找从 $O(n)$ 改善到 $O(\log n)$。当元素减少到阈值以下时,又会退回链表。这个设计充分利用了红黑树"旋转次数有上界"的特性——频繁的插入删除不会导致过高的维护开销。

    延伸
    红黑树的变体与族谱

    红黑树不是孤立存在的。它是更广泛的平衡搜索树族谱的一员:

    graph TD
      BST["二叉搜索树"] --> AVL["AVL 树
    (严格平衡)"] BST --> RBT["红黑树
    (近似平衡)"] BST --> SPLAY["Splay 树
    (自调整)"] BST --> TREAP["Treap
    (随机化)"] RBT --> LLRBT["左倾红黑树
    (Sedgewick 2008)"] RBT --> RBS["标准库实现
    (STL/Java)"] BTree["B 树族"] --> B234["2-3-4 树
    (红黑树等价)"] BTree --> BPlus["B+ 树
    (数据库/文件系统)"] style BST fill:#555,color:#fff style RBT fill:#e74c3c,color:#fff style AVL fill:#27ae60,color:#fff style LLRBT fill:#3498db,color:#fff style B234 fill:#9b59b6,color:#fff

    左倾红黑树(LLRB)是 Robert Sedgewick 在 2008 年提出的变体,强制红色链接只出现在左子节点上。虽然理论上旋转次数略多于标准红黑树,但实现简单得多——插入只需 33 行 Java 代码。LLRB 和 2-3 树(或 2-3-4 树)存在一一对应关系:红色链接对应 2-3 树中的"粘合"节点。本文正是利用了这个对应关系来帮助理解红黑树的操作。

    在更宏观的视角下,B 树族(B 树、B+ 树、B* 树)可以看作红黑树在多路分支上的推广。一棵红黑树等价于一棵特殊的 2-3-4 树,每条红色链接将两个节点"粘合"为一个 3 节点或 4 节点。

    历史脉络:1972 年 Bayer 发明了对称二叉 B 树(2-3-4 树的前身),1978 年 Guibas 和 Sedgewick 从中推导出红黑树。"红色"这个颜色选择据 Sedgewick 本人回忆,只是因为当时 Xerox PARC 的彩色激光打印机打出来的红色最好看。

    一句话总结:红黑树用"颜色"这一看似简单的标记,编码了 2-3-4 树的结构信息。它的五条性质不是凭空设计的约束,而是 2-3-4 树完美平衡性在二叉树形态下的自然映射。理解了这把钥匙,旋转和变色的规则不再需要死记硬背——它们都是 2-3-4 树操作的二叉树翻译。

    参考来源