红黑树(Red-Black Tree)

57 阅读11分钟

红黑树是一种自平衡的二叉搜索树(BST) ,核心目标是解决普通二叉搜索树(BST)“退化为链表” 的性能问题。它通过「节点颜色规则 + 旋转 / 变色调整」,强制将树的高度控制在 O(logn)(n 为节点数),保证插入、删除、查找操作的时间复杂度稳定为 O(logn)。红黑树是工业级应用最广的平衡树(如 C++ STL 的 set/map、Java TreeMap、Linux 内核的内存管理)。

一、红黑树的核心定位

1. 解决的问题:普通 BST 的缺陷

普通 BST 若按有序序列插入(如 1→2→3→4→5),会退化为单链表,树高变为 n,此时查找 / 插入 / 删除的时间复杂度从 O(logn) 退化为 O(n),完全失去二叉树的优势。

2. 红黑树的优势

  • 近似平衡:无需像 AVL 树那样严格控制 “左右子树高度差 ≤1”,仅通过 “黑高一致” 规则控制树高,平衡调整成本更低;
  • 高效操作:插入 / 删除最多只需 2 次旋转,远少于 AVL 树的 O(logn) 次旋转;
  • 稳定性:所有操作的时间复杂度稳定为 O(logn),无最坏情况退化。

二、红黑树的基础定义

1. 节点结构(简化版)

红黑树的节点包含 3 个指针(父 / 左 / 右)、1 个颜色标记、1 个值,且引入「NIL 哨兵节点」(空节点)简化边界处理:

template <typename T>
struct RBNode {
    typedef RBNode* Pointer;
    Pointer parent;   // 父节点指针(回溯/旋转必备)
    Pointer left;     // 左孩子指针
    Pointer right;    // 右孩子指针
    int color;        // 颜色:0=黑色(BLACK),1=红色(RED)
    T key;            // 节点值(用于比较的键)
    static Pointer NIL; // 全局 NIL 哨兵节点(所有空节点指向它)
};

// 初始化 NIL 节点(所有空指针指向它,颜色为黑)
template <typename T>
typename RBNode<T>::Pointer RBNode<T>::NIL = new RBNode<T>{nullptr, nullptr, nullptr, 0, T()};
  • NIL 节点:所有叶子节点的左 / 右指针、根节点的父指针都指向 NIL,统一边界处理(避免判断 nullptr);
  • NIL 节点固定为黑色,是红黑树规则的一部分。

2. 红黑树的 5 条核心规则(平衡的根本)

红黑树的所有平衡调整都是为了满足以下 5 条不变式(规则),缺一不可:

规则编号规则描述规则的核心作用
1每个节点要么是红色,要么是黑色颜色是平衡调整的 “工具”
2根节点必须是黑色避免根节点染红导致全树黑高不一致
3所有 NIL 哨兵节点都是黑色统一叶子节点的边界规则
4若一个节点是红色,则其两个子节点必须是黑色(红节点不能 “相邻”)限制红节点的数量,避免连续红节点
5从任意节点到其所有后代 NIL 节点的路径上,黑色节点的数量完全相同(黑高一致)控制树的最大高度(最长路径 ≤ 2× 最短路径)

关键概念:黑高(Black Height)

  • 定义:从某节点到其后代 NIL 节点的路径上,黑色节点的数量(不包含当前节点);
  • 规则 5 要求:任意节点的所有路径黑高相同,这是红黑树 “近似平衡” 的核心。

3. 红黑树的高度上限(核心推导)

由 5 条规则可推导出:红黑树的高度 h ≤ 2×log₂(n+1) (n 为非 NIL 节点数),保证了操作的 O(logn) 复杂度:

  • 最短路径:全黑节点,长度 = 黑高 bh
  • 最长路径:红黑交替,长度 = 2×bh(规则 4 限制红节点不相邻);
  • 由规则 5,所有路径黑高相同,因此最长路径 ≤ 2× 最短路径,树高不会退化。

三、红黑树的核心操作(旋转 + 插入 + 删除)

红黑树的操作分为「基础操作(查找 / 旋转)」和「核心操作(插入 / 删除)」,其中旋转是平衡调整的唯一 “物理手段” ,变色是辅助手段。

1. 基础操作 1:查找(与普通 BST 完全一致)

红黑树的查找逻辑和普通 BST 无区别,仅利用 “左小右大” 的 BST 特性,时间复杂度 O(logn)

template <typename T>
typename RBNode<T>::Pointer RBTree<T>::search(T key) {
    RBNode<T>::Pointer curr = root;
    while (curr != RBNode<T>::NIL) {
        if (key == curr->key) return curr;       // 找到,返回节点
        else if (key < curr->key) curr = curr->left;  // 往左找
        else curr = curr->right;                     // 往右找
    }
    return RBNode<T>::NIL; // 未找到,返回 NIL
}

2. 基础操作 2:旋转(左旋 / 右旋)

旋转是调整树形态的核心操作,不破坏 BST 特性(左小右大),仅改变节点的父子关系。

(1)左旋(Left Rotate)

以目标节点 x 为轴,将其右孩子 y “提上来” 作为新父节点,x 变为 y 的左孩子,y 的原左孩子变为 x 的右孩子:

template <typename T>
void RBTree<T>::leftRotate(RBNode<T>::Pointer x) {
    RBNode<T>::Pointer y = x->right; // y 是 x 的右孩子
    // 步骤1:y 的左孩子(T2)变为 x 的右孩子
    x->right = y->left;
    if (y->left != RBNode<T>::NIL) {
        y->left->parent = x;
    }
    // 步骤2:y 的父节点替换为 x 的父节点
    y->parent = x->parent;
    if (x->parent == RBNode<T>::NIL) { // x 是根节点
        root = y;
    } else if (x == x->parent->left) { // x 是左孩子
        x->parent->left = y;
    } else { // x 是右孩子
        x->parent->right = y;
    }
    // 步骤3:x 变为 y 的左孩子
    y->left = x;
    x->parent = y;
}

(2)右旋(Right Rotate)

与左旋对称,以目标节点 y 为轴,将其左孩子 x “提上来” 作为新父节点:

template <typename T>
void RBTree<T>::rightRotate(RBNode<T>::Pointer y) {
    RBNode<T>::Pointer x = y->left; // x 是 y 的左孩子
    // 步骤1:x 的右孩子(T2)变为 y 的左孩子
    y->left = x->right;
    if (x->right != RBNode<T>::NIL) {
        x->right->parent = y;
    }
    // 步骤2:x 的父节点替换为 y 的父节点
    x->parent = y->parent;
    if (y->parent == RBNode<T>::NIL) { // y 是根节点
        root = x;
    } else if (y == y->parent->right) { // y 是右孩子
        y->parent->right = x;
    } else { // y 是左孩子
        y->parent->left = x;
    }
    // 步骤3:y 变为 x 的右孩子
    x->right = y;
    y->parent = x;
}

旋转的核心特点

  • 旋转仅调整指针,时间复杂度 O(1)
  • 旋转后仍满足 BST 特性(左子树值 < 父节点值 < 右子树值);
  • 旋转是平衡调整的 “核心工具”,插入 / 删除的平衡调整都依赖旋转。

3. 核心操作 1:插入(Insert)

插入的核心逻辑:先按 BST 插入,再调整颜色 / 旋转,恢复红黑树规则

步骤 1:BST 规则插入新节点

  • 从根节点开始,找到空的插入位置(NIL 节点);
  • 新节点初始颜色设为红色(关键!插入红色节点仅可能违反规则 4,插入黑色节点必然违反规则 5);
  • 若插入位置已存在相同值的节点,直接返回(对应 set 的 “唯一性”)。
template <typename T>
bool RBTree<T>::insert(T key) {
    // 1. 找插入位置(parent 是新节点的父节点)
    RBNode<T>::Pointer parent = RBNode<T>::NIL;
    RBNode<T>::Pointer curr = root;
    while (curr != RBNode<T>::NIL) {
        parent = curr;
        if (key == curr->key) return false; // 重复值,插入失败(set 特性)
        else if (key < curr->key) curr = curr->left;
        else curr = curr->right;
    }
    // 2. 创建新节点(红色)
    RBNode<T>::Pointer newNode = new RBNode<T>{
        parent, RBNode<T>::NIL, RBNode<T>::NIL, 1, key
    };
    // 3. 链接新节点到父节点
    if (parent == RBNode<T>::NIL) {
        root = newNode; // 空树,新节点是根
    } else if (key < parent->key) {
        parent->left = newNode;
    } else {
        parent->right = newNode;
    }
    // 4. 调整平衡,恢复红黑树规则
    insertFixup(newNode);
    return true;
}

步骤 2:插入后的平衡调整(insertFixup)

插入红色节点后,仅可能违反「规则 4(红节点相邻)」或「规则 2(根节点染红)」,需分 3 种核心场景 调整:

场景触发条件调整策略
场景 1叔叔节点是红色父节点 + 叔叔节点 → 黑色;祖父节点 → 红色;将祖父节点作为新节点,继续向上检查
场景 2叔叔节点是黑色,且新节点是 “右孩子”先对父节点左旋,转化为场景 3;再按场景 3 处理
场景 3叔叔节点是黑色,且新节点是 “左孩子”祖父节点 → 红色,父节点 → 黑色;对祖父节点右旋,终止调整

补充:若新节点的父节点是祖父节点的右孩子,场景 2/3 对称(左旋↔右旋)。

平衡调整代码(核心逻辑):

template <typename T>
void RBTree<T>::insertFixup(RBNode<T>::Pointer z) {
    // 仅当父节点是红色时,才需要调整(否则规则 4 不违反)
    while (z->parent->color == 1) {
        if (z->parent == z->parent->parent->left) { // 父节点是祖父的左孩子
            RBNode<T>::Pointer uncle = z->parent->parent->right; // 叔叔节点
            // 场景 1:叔叔是红色 → 变色 + 向上传递
            if (uncle->color == 1) {
                z->parent->color = 0;       // 父节点变黑
                uncle->color = 0;           // 叔叔节点变黑
                z->parent->parent->color = 1; // 祖父节点变红
                z = z->parent->parent;      // 祖父节点作为新节点继续检查
            } else {
                // 场景 2:叔叔是黑色,z 是右孩子 → 左旋父节点,转场景 3
                if (z == z->parent->right) {
                    z = z->parent;
                    leftRotate(z);
                }
                // 场景 3:叔叔是黑色,z 是左孩子 → 变色 + 右旋祖父节点
                z->parent->color = 0;
                z->parent->parent->color = 1;
                rightRotate(z->parent->parent);
            }
        } else { // 父节点是祖父的右孩子(对称逻辑)
            RBNode<T>::Pointer uncle = z->parent->parent->left;
            if (uncle->color == 1) {
                z->parent->color = 0;
                uncle->color = 0;
                z->parent->parent->color = 1;
                z = z->parent->parent;
            } else {
                if (z == z->parent->left) {
                    z = z->parent;
                    rightRotate(z);
                }
                z->parent->color = 0;
                z->parent->parent->color = 1;
                leftRotate(z->parent->parent);
            }
        }
    }
    // 确保根节点是黑色(规则 2)
    root->color = 0;
}

插入示例(直观理解)

插入前红黑树:

        4 (黑)
       /   \
      2(红) 6(红)

插入节点 5(红色)→ 父节点 6 是红色,违反规则 4:

  • 叔叔节点是 2(红色)→ 场景 1:2、6 变黑色,4 变红色;
  • 4 是根节点 → 强制改回黑色;最终平衡后的树:
        4 (黑)
       /   \
      2(黑) 6(黑)
           /
          5(红)

4. 核心操作 2:删除(Delete)

删除是红黑树最复杂的操作,核心逻辑:先按 BST 删除节点,再修复红黑树规则

步骤 1:BST 规则删除节点

按待删除节点的子节点数量,分 3 种情况(与普通 BST 一致):

  1. 叶子节点(无孩子) :直接删除,父节点的对应指针指向 NIL;
  2. 单孩子节点:将父节点的指针指向该节点的子节点,再删除原节点;
  3. 双孩子节点:找到「中序后继」(右子树的最左节点),用后继节点的值覆盖当前节点,再删除后继节点(后继节点必是情况 1/2)。

步骤 2:删除后的平衡调整(deleteFixup)

删除节点后,仅当「删除的是黑色节点」时,才会破坏规则 5(黑高不一致),需要修复;若删除的是红色节点,规则无破坏,直接结束。

修复的核心思路:通过 “借黑”“旋转 + 变色”,让所有路径的黑高恢复一致,分 4 种核心场景(对称共 8 种):

场景触发条件调整策略
场景 1兄弟节点是红色兄弟节点 → 黑色,父节点 → 红色;对父节点左旋,转化为场景 2/3/4
场景 2兄弟节点是黑色,且兄弟的两个孩子都是黑色兄弟节点 → 红色;将父节点作为新节点,继续向上检查
场景 3兄弟节点是黑色,兄弟的左孩子红、右孩子黑兄弟节点 → 红色,左孩子 → 黑色;对兄弟节点右旋,转化为场景 4
场景 4兄弟节点是黑色,兄弟的右孩子红兄弟节点继承父节点颜色,父节点 → 黑色,兄弟的右孩子 → 黑色;对父节点左旋,终止调整

删除的核心特点

  • 平衡调整最多需要 2 次旋转;
  • 删除后,除被删除节点的迭代器外,其他迭代器均不失效(红黑树仅调整指针,未移动节点内存)。

四、红黑树 vs AVL 树(核心对比)

特性红黑树AVL 树
平衡标准黑高一致(近似平衡)左右子树高度差 ≤1(严格平衡)
树高上限2×log₂(n+1)1.44×log₂(n+1)
插入旋转次数最多 2 次最多 O (logn) 次
删除旋转次数最多 2 次最多 O (logn) 次
查找效率略低(树高稍高)略高(更平衡)
适用场景频繁插入 / 删除(如 set/map)频繁查找(如数据库索引)

五、红黑树的工业级应用

  1. C++ STLset/map/multiset/multimap 的底层实现;
  2. Java 集合TreeMap/TreeSet 的底层实现;
  3. Linux 内核:内存管理(slab 分配器)、进程调度的红黑树;
  4. 数据库:MySQL 的索引(B+ 树的基础是平衡树思想,红黑树是核心参考);
  5. 分布式系统:Redis 的有序集合(zset)在元素较多时,底层用红黑树实现。

六、核心总结

  1. 红黑树是「近似平衡」的 BST,通过 5 条颜色规则控制树高在 O(logn)
  2. 旋转是平衡调整的核心工具,不破坏 BST 特性,时间复杂度 O(1)
  3. 插入:先 BST 插入(红节点),再修复规则 4,最多 2 次旋转;
  4. 删除:先 BST 删除,再修复规则 5(仅删黑节点时),最多 2 次旋转;
  5. 红黑树的优势是「插入 / 删除调整成本低」,是工业级平衡树的首选。

理解红黑树的核心是:颜色规则是 “平衡的约束”,旋转 / 变色是 “平衡的手段”,最终目标是维持树高 O (logn),保证操作高效