Skip to content

红黑树

平衡与维护成本的权衡

AVL 树够平衡,但删除一个结点可能触发从叶子到根的连锁旋转——Linux 内核的进程调度、Java 的 TreeMap、C++ 的 std::map 都不愿意为每次删除付这个代价。红黑树的策略是放松平衡条件:不要求左右子树高度严格相等,只要求任意路径上的黑色结点数相同。代价是树可能比 AVL 高出一倍(最坏 2log₂(n+1) vs 1.44log₂(n+2)),但插入最多转 2 次、删除最多转 3 次,维护成本可控。

408 考研中,红黑树是树与二叉树章节的重要考点,常与 AVL 树对比考查。

核心思想

红黑树的核心特点:

  • 每个节点非红即黑:用 1 bit 额外空间存储颜色信息
  • 弱平衡策略:不要求左右子树高度严格相等,只要求任意路径上的黑色节点数相同(黑高相等)
  • 最长路径 ≤ 2 × 最短路径:由性质推导得出,保证最坏情况下仍为 O(log n)

黑高(Black Height) 的定义:从某节点出发(不含该节点)到任一叶节点路径上的黑色节点数。整棵树的黑高 = 根节点的黑高。

节点结构:

c
typedef enum { RED, BLACK } Color;

typedef struct RBNode {
    int key;
    Color color;
    struct RBNode *left, *right, *parent;
} RBNode;

交互可视化

通过下方的交互动画,你可以逐步观察红黑树的执行过程:

加载可视化中...

操作详解

五条性质

红黑树必须同时满足以下五条性质:

编号性质说明
每个节点是红色或黑色用颜色标记区分节点
根节点是黑色根必须为黑
叶节点(NIL)是黑色这里的叶节点指外部的空节点,不是普通叶节点
红节点的两个子节点都是黑色即不存在连续的两个红节点("不红红")
从任一节点到其所有叶节点的路径上,黑色节点数目相同即黑高相等

⚠️ 易错:性质③中的'叶结点'是指 NIL 空结点(外部结点),不是通常意义上的叶子结点(没有子树的内部结点)。408 题目说'红黑树的叶结点是黑色'指的就是这些 NIL 结点。画红黑树时一定要把 NIL 结点画出来,否则容易在计算黑高时出错。

关键推论:由性质④和⑤可得——最长路径(红黑交替)不超过最短路径(全黑)的 2 倍,因此含 n 个内部节点的红黑树高度 h ≤ 2log₂(n+1)

插入操作

红黑树的插入分两步:按 BST 规则插入 → 着色并调整

关键步骤:

  1. 按照二叉搜索树的规则找到插入位置
  2. 新节点染为红色(若染黑则必破坏性质⑤,染红只可能破坏性质④)
  3. 若破坏了红黑性质,则进行调整(变色 / 旋转)

⚠️ 易错:新插入结点必须染红。如果染黑,则该路径的黑高比其他路径多 1,必定破坏性质⑤;染红则只可能破坏性质④(当父结点也是红色时)。408 选择题常问'为什么新结点染红而不染黑'。

c
// 插入节点(简化版)
void rbInsert(RBTree *T, int key) {
    RBNode *z = createNode(key);
    z->color = RED;  // 新节点染红
    // 按 BST 规则插入 z
    bstInsert(T, z);
    // 插入后调整
    rbInsertFixup(T, z);
}

插入后调整

插入红色节点后,只有当父节点也是红色时才需要调整(违反性质④)。设当前节点为 z,根据叔节点(uncle)的颜色分三种情况:

情况一:叔节点为红色(变色即可)

  • 将父节点和叔节点染,祖父节点染
  • z 指向祖父节点,继续向上检查
        G(黑)              G(红) ← 继续向上
       / \      变色       / \
      P(红) U(红)  →     P(黑) U(黑)
     /                  /
    z(红)              z(红)

情况二:叔节点为黑色,z 是"折线"形(先旋转变直线)

  • z 是父节点的右孩子(父是祖父的左孩子),对 P 左旋,转化为情况三
  • (镜像情况类似,对 P 右旋)

情况三:叔节点为黑色,z 是"直线"形(旋转 + 变色)

  • 对祖父节点 G 右旋(若 P 是 G 的左孩子)
  • P 染黑,G 染红
        G(黑)              P(黑)
       / \     右旋G      / \
      P(红) U(黑)  →    z(红) G(红)
     /                        \
    z(红)                      U(黑)

插入调整最多旋转 2 次,其余操作为 O(log n) 的变色上溯。

c
void rbInsertFixup(RBTree *T, RBNode *z) {
    while (z->parent && z->parent->color == RED) {
        if (z->parent == z->parent->parent->left) {
            RBNode *uncle = z->parent->parent->right;
            if (uncle && uncle->color == RED) {
                // 情况一:叔节点红色
                z->parent->color = BLACK;
                uncle->color = BLACK;
                z->parent->parent->color = RED;
                z = z->parent->parent;
            } else {
                if (z == z->parent->right) {
                    // 情况二:折线 → 左旋转为直线
                    z = z->parent;
                    leftRotate(T, z);
                }
                // 情况三:直线 → 右旋 + 变色
                z->parent->color = BLACK;
                z->parent->parent->color = RED;
                rightRotate(T, z->parent->parent);
            }
        } else {
            // 镜像处理(parent 是右孩子,左右互换)
            // ... 对称操作
        }
    }
    T->root->color = BLACK;  // 保证性质②
}

删除操作

红黑树的删除分两步:先按 BST 规则删除节点,再修复可能被破坏的红黑性质。

BST 删除的三种情况(与普通 BST 相同):

  • 被删节点无孩子 → 直接删除
  • 被删节点有一个孩子 → 用孩子替代
  • 被删节点有两个孩子 → 找中序后继(或前驱),把后继的值复制过来,转化为删除后继节点(后继节点最多只有一个右孩子)

删完之后,关键在于被实际移除的节点是什么颜色

  • 删除的是红色节点 → 黑高不变,五条性质均满足,直接结束
  • 删除的是黑色节点 → 从该节点到叶子的所有路径黑高减 1,破坏性质⑤,需要调整

调整时引入一个概念:设 x 为替代被删黑色节点位置的节点(可以是真实节点,也可以是 NIL 哨兵)。x 额外"携带"了一重黑色(双重黑),目标是通过旋转和变色消除这重多余的黑色。调整依据的是 x兄弟节点颜色,共四种情况。

⚠️ 易错:插入调整看叔父节点颜色,删除调整看兄弟节点颜色,两者经常混淆。

删除后调整

x 是其父节点的左孩子为例,右孩子情况完全对称。设 wx 的兄弟节点。


情况 1:兄弟 w 是红色

       P(黑)                   W(黑)
      /     \       →         /     \
    x(双黑)  W(红)           P(红)   Wr
            / \             /   \
           Wl  Wr         x(双黑) Wl

操作:w 染黑,P(父节点)染红,对 P 左旋。旋转后 x 的新兄弟是原来的 Wl,为黑色,转化为情况 2/3/4 继续处理。


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

        P(任意色)                  P(双黑或根)
       /         \       →        /          \
    x(双黑)      W(黑)          x(普通黑)    W(红)
                /   \                       /   \
             Wl(黑) Wr(黑)               Wl(黑) Wr(黑)

操作:w 染红,x 的双重黑上溯给父节点 P。若 P 原本是红色,直接染黑,调整结束;若 P 是黑色,则 P 变为新的双重黑节点,继续向上迭代。最坏情况迭代到根,根直接去掉一重黑(整棵树黑高减 1,仍合法)。


情况 3:兄弟 w 是黑色,w 的左孩子(近侄)红,右孩子(远侄)黑

        P                      P
       / \                    / \
      x   W(黑)     →        x   Wl(黑)
         / \                       \
       Wl(红) Wr(黑)               W(红)
                                     \
                                     Wr(黑)

操作:Wl(近侄)染黑,w 染红,对 w 右旋。转化为情况 4(兄弟黑 + 远侄红)。


情况 4:兄弟 w 是黑色,w 的右孩子(远侄)红

        P(任意色)                   W
       /         \       →        /   \
     x(双黑)     W(黑)           P(黑)  Wr(黑)
                /   \           /   \
              Wl   Wr(红)     x(黑)  Wl

操作:wP 的颜色,P 染黑,Wr(远侄)染黑,对 P 左旋。x 的双重黑消除,调整结束


四种情况的转化关系:

情况1 ──→ 情况2/3/4
情况3 ──→ 情况4
情况4 ──────────→ 结束(常数步)
情况2 ──→ 上溯(最多 O(log n) 次)

⚠️ 易错:情况 2 是唯一可能导致迭代的情况,其余三种(1/3/4)都能在常数步内终止。所以删除旋转次数最多 3 次(情况 1 → 情况 3 → 情况 4,每次 1 次旋转)。

复杂度分析

操作时间复杂度说明
查找O(log n)树高 ≤ 2log₂(n+1)
插入O(log n)查找位置 O(log n) + 调整最多旋转 2 次
删除O(log n)查找位置 O(log n) + 调整最多旋转 3 次

红黑树 vs AVL 树对比

对比项红黑树AVL 树
平衡条件黑高相等(弱平衡)左右子树高度差 ≤ 1(严格平衡)
最大高度2log₂(n+1)1.44log₂(n+2)
查找效率略低于 AVL更优
插入旋转次数≤ 2 次≤ 2 次
删除旋转次数≤ 3 次O(log n) 次
适用场景频繁插入/删除(如 Linux 内核、STL map)查找密集型场景

⚠️ 易错:红黑树的最大高度是 2log₂(n+1),AVL 树是约 1.44log₂n。所以对于查找密集型操作,AVL 树效率更高;对于频繁插入删除的场景,红黑树更优(旋转次数有常数上界)。408 简答题常考'在什么场景下选 AVL,什么场景下选红黑树'。

考研高频考点

  • ⭐ 红黑树的五条性质(选择题/判断题高频,务必逐条记牢)
  • ⭐ 含 n 个内部节点的红黑树高度上界 h ≤ 2log₂(n+1)(填空题/证明题)
  • ⭐ 红黑树 vs AVL 树的对比:平衡条件、旋转次数、适用场景(简答题/选择题常考)
  • ⭐ 插入调整三种情况的判断与操作(叔红变色、叔黑折线双旋、叔黑直线单旋)
  • ⭐ 删除调整四种情况的判断(兄弟红→转化;兄弟黑+双黑侄→上溯;兄弟黑+近红远黑→双旋;兄弟黑+远红→单旋)
  • 黑高的定义与计算(概念题)
  • 新插入节点为什么染红而不染黑(选择题偶考)
  • 红黑树中最短路径与最长路径的关系(最长 ≤ 2 × 最短)

相关知识

  • 红黑树的插入调整建立在 BST 插入和旋转操作的基础上,与 AVL 树的四种旋转本质相同,只是触发条件和调整策略不同
  • 红黑树与 B 树存在等价关系:一棵红黑树可以等价转化为一棵 2-3-4 树(4 阶 B 树),红色结点与其黑色父结点"合并"即可得到 B 树结点
  • 红黑树的 O(log n) 查找性能与哈希表的 O(1) 查找形成对比——红黑树胜在有序性(支持范围查询和有序遍历),哈希表胜在单点查找速度