AIGC标识 红黑树

C++ 红黑树详解

目录

  1. 引言
  2. 红黑树的历史与定义
  3. 红黑树的五大性质
  4. 红黑树的高度与时间复杂度证明
  5. 红黑树的节点结构
  6. 旋转操作
  7. 红黑树的插入操作
  8. 红黑树的删除操作
  9. 红黑树的查找操作
  10. 红黑树与2-3-4树的等价性
  11. 红黑树与AVL树的对比
  12. C++中红黑树的实现
  13. STL中map与set的底层实现
  14. 红黑树的应用场景
  15. 总结

1. 引言

在计算机科学中,二叉搜索树(Binary Search Tree, BST)是一种基础且重要的数据结构,它支持高效的查找、插入和删除操作。然而,普通的二叉搜索树在最坏情况下(例如插入有序数据时)会退化为链表,导致操作时间复杂度从理想的 \(O(\log n)\) 恶化到 \(O(n)\)。

为了解决这一问题,计算机科学家们发明了各种自平衡二叉搜索树(Self-balancing Binary Search Tree)。其中,红黑树(Red-Black Tree)是最为著名和广泛应用的一种。它通过对节点着色(红色或黑色)并施加一组约束规则,确保树在任何时候都保持近似平衡,从而保证基本动态集合操作在最坏情况下的时间复杂度为 \(O(\log n)\)。

红黑树在1972年由鲁道夫·贝尔(Rudolf Bayer)发明,当时被称为对称二叉B树(Symmetric Binary B-tree)。1978年,莱奥·吉巴斯(Leo J. Guibas)和罗伯特·塞奇威克(Robert Sedgewick)对其进行了改进并重新命名为“红黑树”。如今,红黑树已成为许多编程语言标准库中关联容器(如C++的 std::map、std::set,Java的 TreeMap、TreeSet)的底层实现。

红黑树定义(Red-Black Tree Definition):红黑树是一棵二叉搜索树,它在每个节点上增加了一个存储位来表示节点的颜色(红色或黑色),并通过红黑性质(Red-Black Properties)的约束,确保没有一条从根到叶子的路径会比其他路径长出两倍,因而是近似平衡的。

2. 红黑树的历史与定义

2.1 历史背景

  • 1972年:鲁道夫·贝尔发明了“对称二叉B树”。
  • 1978年:吉巴斯和塞奇威克将其改进为红黑树。
  • 如今:红黑树已成为最广泛使用的平衡树之一,被广泛应用于操作系统内核、数据库系统、编程语言标准库等领域。

2.2 为什么需要红黑树

二叉搜索树的性能取决于树的高度 \(h\)。当树保持平衡时,\(h = O(\log n)\),操作高效;但当树倾斜时,\(h\) 可能达到 \(O(n)\),性能严重退化。

红黑树通过颜色约束和旋转操作来维持树的平衡,而不像AVL树那样要求严格的平衡因子控制。这种“近似平衡”的策略在保证 \(O(\log n)\) 性能的同时,减少了平衡调整的开销。

3. 红黑树的五大性质

一棵红黑树必须满足以下五个红黑性质(Red-Black Properties):

性质1(节点颜色):每个节点要么是红色,要么是黑色。

性质2(根节点):根节点是黑色。

性质3(叶子节点):每个叶子节点(NIL)是黑色。这里的叶子节点指的是不存储任何关键字的空节点(哨兵节点),而非传统意义上的叶节点。

性质4(红色约束):如果一个节点是红色,那么它的两个子节点都是黑色。等价地说,红色节点的父节点必须是黑色,即不存在两个连续的红色节点。

性质5(黑色高度一致):对每个节点,从该节点到其所有后代叶子节点的路径上,包含相同数目的黑色节点。

注:在实际实现中,常用一个哨兵节点(Sentinel Node)NIL 来代表所有叶子节点。该哨兵节点是黑色的,不存储实际数据。所有实际存储关键字的节点称为内部节点(Internal Node)。

4. 红黑树的高度与时间复杂度证明

4.1 黑高度(Black Height)的定义

定义(黑高度):从某个节点 \(x\) 出发(不包含节点 \(x\) 本身),到达其任意一个后代叶子节点(NIL)的路径上,黑色节点的个数称为节点 \(x\) 的黑高度(Black Height),记为 \(bh(x)\)。

由性质5可知,从同一节点出发到所有叶子节点的路径上黑色节点数相同,因此 \(bh(x)\) 的定义是明确的。

红黑树的黑高度定义为根节点的黑高度 \(bh(root)\)。

4.2 核心引理

引理 1:一棵以节点 \(x\) 为根的子树至少包含 \(2^{bh(x)} - 1\) 个内部节点。

证明(数学归纳法):

  • 基础情况:当 \(bh(x) = 0\) 时,\(x\) 是叶子节点(NIL),内部节点数为 \(0\),而 \(2^0 - 1 = 0\),成立。

  • 归纳步骤:假设对于黑高度为 \(bh(x) - 1\) 的节点,命题成立。考虑节点 \(x\),其黑高度为 \(bh(x) > 0\)。\(x\) 的两个子树的黑高度为 \(bh(x)\) 或 \(bh(x) - 1\)(取决于子节点是否为红色)。根据归纳假设,每个子树至少包含 \(2^{bh(x)-1} - 1\) 个内部节点。因此,以 \(x\) 为根的子树至少包含:

\[(2^{bh(x)-1} - 1) + (2^{bh(x)-1} - 1) + 1 = 2^{bh(x)} - 1 \]

个内部节点。

由归纳法,引理得证。

4.3 红黑树高度上界

定理 1:一棵含有 \(n\) 个内部节点的红黑树,其高度 \(h\) 至多为 \(2\log_2(n+1)\)。

证明:

设红黑树的高度为 \(h\),黑高度为 \(bh\)。根据性质4(不存在两个连续的红色节点),在从根到叶子的任何路径上,至少有一半的节点是黑色。因此:

\[bh \geq \frac{h}{2} \]

由引理1,以根为根的子树(即整棵树)至少包含 \(2^{bh} - 1\) 个内部节点:

\[n \geq 2^{bh} - 1 \geq 2^{h/2} - 1 \]

整理得:

\[n + 1 \geq 2^{h/2} \]

两边取以2为底的对数:

\[\log_2(n+1) \geq \frac{h}{2} \]

\[h \leq 2\log_2(n+1) \]

定理得证。

4.4 时间复杂度结论

由定理1可知,红黑树的高度 \(h = O(\log n)\)。而红黑树上的查找、插入、删除操作的时间复杂度均与树的高度成正比。

推论:红黑树的查找、插入、删除操作在最坏情况下的时间复杂度均为 \(O(\log n)\)。

5. 红黑树的节点结构

5.1 C++节点定义

在C++中实现红黑树,首先需要定义节点结构。每个节点通常包含以下成员:

// 颜色枚举
enum Color { RED, BLACK };

// 红黑树节点模板
template<typename K, typename V>
struct RBTreeNode {
    // 键值对
    std::pair<K, V> _kv;
    
    // 三叉链:左孩子、右孩子、父节点
    RBTreeNode<K, V>* _left;
    RBTreeNode<K, V>* _right;
    RBTreeNode<K, V>* _parent;
    
    // 节点颜色
    Color _col;
    
    // 构造函数:新节点默认为红色
    RBTreeNode(const std::pair<K, V>& kv)
        : _kv(kv)
        , _left(nullptr)
        , _right(nullptr)
        , _parent(nullptr)
        , _col(RED) {}
};

5.2 为什么新节点默认为红色

新插入的节点默认设置为红色,原因如下:

  • 如果插入黑色节点,会立即破坏性质5(黑色高度一致),因为该路径上的黑色节点数比其他路径多1,必须进行调整。
  • 如果插入红色节点,只有在父节点也是红色时才会破坏性质4(无连续红色),这并非必然发生。因此,将新节点设为红色可以减少调整的频率和复杂度。

专业术语解释:

  • 三叉链(Trichain):节点包含指向左孩子、右孩子和父节点的三个指针,便于在插入和删除时进行回溯操作。
  • 键值对(Key-Value Pair):std::pair<K, V> 是C++标准库中用于存储两个相关联值的模板类。

6. 旋转操作

旋转(Rotation)是红黑树维护平衡的基本操作。当插入或删除节点破坏了红黑性质时,通过旋转和重新着色来恢复平衡。

旋转操作分为左旋(Left Rotation)和右旋(Right Rotation)两种。

6.1 左旋(Left Rotation)

左旋定义:以某个节点 \(x\) 为支点进行左旋,将 \(x\) 的右孩子 \(y\) 提升为 \(x\) 的父节点,\(x\) 成为 \(y\) 的左孩子。\(y\) 原来的左子树 \(\beta\) 成为 \(x\) 的右子树。

左旋示意图:

       x                         y
      / \                       / \
     α   y       ====>         x   γ
        / \                   / \
       β   γ                 α   β

左旋的C++实现:

void rotateLeft(Node* x) {
    Node* y = x->_right;      // y 是 x 的右孩子
    x->_right = y->_left;     // 将 y 的左子树挂到 x 的右子树
    
    if (y->_left != nullptr) {
        y->_left->_parent = x;
    }
    
    y->_parent = x->_parent;  // 更新 y 的父指针
    
    if (x->_parent == nullptr) {
        _root = y;            // x 是根节点
    } else if (x == x->_parent->_left) {
        x->_parent->_left = y;
    } else {
        x->_parent->_right = y;
    }
    
    y->_left = x;             // x 成为 y 的左孩子
    x->_parent = y;
}

6.2 右旋(Right Rotation)

右旋定义:以某个节点 \(y\) 为支点进行右旋,将 \(y\) 的左孩子 \(x\) 提升为 \(y\) 的父节点,\(y\) 成为 \(x\) 的右孩子。\(x\) 原来的右子树 \(\beta\) 成为 \(y\) 的左子树。

右旋示意图:

       y                         x
      / \                       / \
     x   γ       ====>         α   y
    / \                           / \
   α   β                         β   γ

右旋的C++实现:

void rotateRight(Node* y) {
    Node* x = y->_left;       // x 是 y 的左孩子
    y->_left = x->_right;     // 将 x 的右子树挂到 y 的左子树
    
    if (x->_right != nullptr) {
        x->_right->_parent = y;
    }
    
    x->_parent = y->_parent;  // 更新 x 的父指针
    
    if (y->_parent == nullptr) {
        _root = x;            // y 是根节点
    } else if (y == y->_parent->_left) {
        y->_parent->_left = x;
    } else {
        y->_parent->_right = x;
    }
    
    x->_right = y;            // y 成为 x 的右孩子
    y->_parent = x;
}

6.3 旋转的性质

旋转不变量:左旋和右旋操作不改变二叉搜索树的中序遍历顺序。也就是说,旋转操作保持二叉搜索树的性质不变。

旋转的复杂度:左旋和右旋操作都只需要修改常数个指针,时间复杂度为 \(O(1)\)。

记忆技巧:

  • 左旋:将右孩子“旋”上来,自己变成左孩子。
  • 右旋:将左孩子“旋”上来,自己变成右孩子。

7. 红黑树的插入操作

红黑树的插入操作分为两个阶段:

  1. 标准BST插入:按照二叉搜索树的规则插入新节点。
  2. 修复平衡:通过变色和旋转恢复红黑性质。

7.1 标准BST插入

新节点按照二叉搜索树的规则插入,颜色设为红色。

void insert(const std::pair<K, V>& kv) {
    Node* newNode = new Node(kv);  // 新节点默认为红色
    
    if (_root == nullptr) {
        _root = newNode;
        _root->_col = BLACK;       // 根节点必须为黑色
        return;
    }
    
    Node* cur = _root;
    Node* parent = nullptr;
    
    // 按照BST规则查找插入位置
    while (cur != nullptr) {
        parent = cur;
        if (kv.first < cur->_kv.first) {
            cur = cur->_left;
        } else if (kv.first > cur->_kv.first) {
            cur = cur->_right;
        } else {
            // 键已存在,不允许重复插入
            delete newNode;
            return;
        }
    }
    
    // 插入新节点
    newNode->_parent = parent;
    if (kv.first < parent->_kv.first) {
        parent->_left = newNode;
    } else {
        parent->_right = newNode;
    }
    
    // 修复红黑性质
    fixAfterInsertion(newNode);
}

7.2 插入修复的三种情况

插入红色节点后,只有性质4(无连续红色)可能被破坏。修复操作根据父节点和叔叔节点(Uncle Node,即祖父节点的另一个子节点)的颜色,分为三种情况。

术语约定:

  • 当前节点 \(z\):新插入的红色节点。
  • 父节点 \(p\):\(z\) 的父节点。
  • 祖父节点 \(g\):\(p\) 的父节点。
  • 叔叔节点 \(u\):\(g\) 的另一个子节点(即 \(p\) 的兄弟节点)。

情况1:叔叔节点是红色

当父节点和叔叔节点都是红色时,只需进行颜色翻转(Color Flip):

  1. 将父节点 \(p\) 设为黑色。
  2. 将叔叔节点 \(u\) 设为黑色。
  3. 将祖父节点 \(g\) 设为红色(除非 \(g\) 是根节点)。
  4. 将 \(g\) 作为新的当前节点,向上回溯继续检查。
     [g(B)]                    [g(R)]
     /     \        ====>      /     \
   [p(R)]  [u(R)]            [p(B)]  [u(B)]
   /                          /
 [z(R)]                    [z(R)]

情况2:叔叔节点是黑色,且当前节点是父节点的右孩子("LR"型)

这种情况需要先进行左旋,将"LR"型转化为"LL"型:

  1. 以父节点 \(p\) 为支点进行左旋。
  2. 旋转后,原来的当前节点 \(z\) 成为新的父节点,原来的父节点 \(p\) 成为 \(z\) 的左孩子。
  3. 进入情况3处理。
      [g(B)]                       [g(B)]
      /                            /
    [p(R)]        ====>          [z(R)]
      \                          /
      [z(R)]                  [p(R)]

情况3:叔叔节点是黑色,且当前节点是父节点的左孩子("LL"型)

这是可以直接修复的情况:

  1. 将父节点 \(p\) 设为黑色。
  2. 将祖父节点 \(g\) 设为红色。
  3. 以祖父节点 \(g\) 为支点进行右旋。
      [g(B)]                    [p(B)]
      /                         /     \
    [p(R)]        ====>      [z(R)]  [g(R)]
    /
  [z(R)]

7.3 插入修复的完整C++实现

void fixAfterInsertion(Node* z) {
    // 当父节点存在且为红色时,需要修复
    while (z != _root && z->_parent->_col == RED) {
        Node* parent = z->_parent;
        Node* grandparent = parent->_parent;
        
        if (parent == grandparent->_left) {
            // 父节点是祖父节点的左孩子
            Node* uncle = grandparent->_right;
            
            if (uncle != nullptr && uncle->_col == RED) {
                // 情况1:叔叔节点是红色 → 颜色翻转
                parent->_col = BLACK;
                uncle->_col = BLACK;
                grandparent->_col = RED;
                z = grandparent;  // 向上回溯
            } else {
                // 叔叔节点是黑色或不存在
                if (z == parent->_right) {
                    // 情况2:"LR"型 → 左旋
                    rotateLeft(parent);
                    // 交换z和parent,转化为情况3
                    std::swap(z, parent);
                }
                // 情况3:"LL"型 → 变色+右旋
                parent->_col = BLACK;
                grandparent->_col = RED;
                rotateRight(grandparent);
            }
        } else {
            // 父节点是祖父节点的右孩子(对称情况)
            Node* uncle = grandparent->_left;
            
            if (uncle != nullptr && uncle->_col == RED) {
                // 情况1对称:叔叔节点是红色
                parent->_col = BLACK;
                uncle->_col = BLACK;
                grandparent->_col = RED;
                z = grandparent;
            } else {
                if (z == parent->_left) {
                    // "RL"型 → 右旋
                    rotateRight(parent);
                    std::swap(z, parent);
                }
                // "RR"型 → 变色+左旋
                parent->_col = BLACK;
                grandparent->_col = RED;
                rotateLeft(grandparent);
            }
        }
    }
    
    // 确保根节点为黑色(性质2)
    _root->_col = BLACK;
}

7.4 插入操作的复杂度

  • 时间复杂度:\(O(\log n)\),因为修复过程中最多进行 \(O(\log n)\) 次颜色翻转,且最多进行2次旋转。
  • 空间复杂度:\(O(1)\),只使用了常数个额外空间。

8. 红黑树的删除操作

红黑树的删除操作是最复杂的操作,需要处理多种情况。

8.1 标准BST删除

红黑树的删除首先按照二叉搜索树的删除规则进行:

  1. 情况A:待删除节点 \(z\) 没有孩子 → 直接删除。
  2. 情况B:待删除节点 \(z\) 只有一个孩子 → 用孩子替换 \(z\)。
  3. 情况C:待删除节点 \(z\) 有两个孩子 → 找到 \(z\) 的后继节点(或前驱节点),用后继节点的值替换 \(z\) 的值,然后删除后继节点(后继节点最多只有一个孩子,转化为情况A或B)。

8.2 删除修复的核心问题

删除操作对红黑性质的影响取决于被删除节点的颜色:

  • 删除红色节点:不会破坏任何红黑性质,无需修复。
  • 删除黑色节点:会破坏性质5(黑色高度一致),因为被删除节点所在路径上的黑色节点数比其他路径少1。需要进行修复。

核心思想:删除黑色节点后,让替代节点 "继承"一重黑色,即认为替代节点具有"双重黑色"(Double Black)或"黑+黑"(Black-plus-Black)的属性。修复过程就是消除这重额外的黑色。

8.3 删除修复的四种情况

设 \(x\) 是替代被删除节点的节点(即实际占据被删除节点位置的节点),\(x\) 具有"双重黑色"属性(除非 \(x\) 是红色,可直接涂黑解决)。

术语约定:

  • 当前节点 \(x\):具有"双重黑色"的节点。
  • 兄弟节点 \(w\):\(x\) 的兄弟节点。
  • 父节点 \(p\):\(x\) 和 \(w\) 的父节点。

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

  1. 将兄弟节点 \(w\) 设为黑色。
  2. 将父节点 \(p\) 设为红色。
  3. 以父节点 \(p\) 为支点进行左旋(假设 \(x\) 是左孩子)。
  4. 更新兄弟节点 \(w\) 为 \(p\) 的新右孩子。
  5. 转化为情况2、3或4处理。
       [p(B)]                        [w(B)]
       /     \          ====>        /     \
    [x(BB)] [w(R)]               [p(R)]  [γ]
            /  \                  /  \
          [α]  [γ]            [x(BB)] [α]

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

  1. 将兄弟节点 \(w\) 设为红色。
  2. 将 \(x\) 的"双重黑色"中的一重黑色上移到父节点 \(p\)。
  3. 如果 \(p\) 原来是红色,将 \(p\) 设为黑色,结束;如果 \(p\) 原来是黑色,则 \(p\) 成为新的"双重黑色"节点,向上回溯。
       [p(任意)]                    [p(任意+1黑)]
       /     \          ====>        /     \
    [x(BB)]  [w(B)]               [x(B)]  [w(R)]
            /  \                          /  \
          [α]  [γ]                     [α]  [γ]

情况3:兄弟节点 \(w\) 是黑色,\(w\) 的左孩子是红色,右孩子是黑色

  1. 将 \(w\) 的左孩子设为黑色。
  2. 将 \(w\) 设为红色。
  3. 以 \(w\) 为支点进行右旋。
  4. 更新兄弟节点 \(w\) 为 \(p\) 的新右孩子。
  5. 转化为情况4处理。
       [p]                            [p]
       /                              /
    [x(BB)]  [w(B)]    ====>      [x(BB)]  [α(R)]
             /  \                             \
          [α(R)] [γ]                          [w(B)]
                                                \
                                                [γ]

情况4:兄弟节点 \(w\) 是黑色,且 \(w\) 的右孩子是红色

这是可以直接修复的情况:

  1. 将 \(w\) 的颜色设为 \(p\) 的颜色。
  2. 将 \(p\) 设为黑色。
  3. 将 \(w\) 的右孩子设为黑色。
  4. 以 \(p\) 为支点进行左旋。
  5. 消除 \(x\) 的"双重黑色",结束。
       [p(c)]                         [w(c)]
       /     \          ====>        /     \
    [x(BB)]  [w(B)]               [p(B)]  [γ(B)]
            /  \                  /  \
          [α]  [γ(R)]          [x(B)] [α]

8.4 删除修复的完整C++实现

void fixAfterDeletion(Node* x) {
    while (x != _root && (x == nullptr || x->_col == BLACK)) {
        if (x == x->_parent->_left) {
            // x 是左孩子
            Node* w = x->_parent->_right;  // 兄弟节点
            
            if (w != nullptr && w->_col == RED) {
                // 情况1:兄弟节点是红色
                w->_col = BLACK;
                x->_parent->_col = RED;
                rotateLeft(x->_parent);
                w = x->_parent->_right;
            }
            
            if ((w->_left == nullptr || w->_left->_col == BLACK) &&
                (w->_right == nullptr || w->_right->_col == BLACK)) {
                // 情况2:兄弟节点的两个孩子都是黑色
                if (w != nullptr) {
                    w->_col = RED;
                }
                x = x->_parent;  // 向上回溯
            } else {
                if (w->_right == nullptr || w->_right->_col == BLACK) {
                    // 情况3:兄弟节点的右孩子是黑色,左孩子是红色
                    if (w->_left != nullptr) {
                        w->_left->_col = BLACK;
                    }
                    w->_col = RED;
                    rotateRight(w);
                    w = x->_parent->_right;
                }
                // 情况4:兄弟节点的右孩子是红色
                w->_col = x->_parent->_col;
                x->_parent->_col = BLACK;
                if (w->_right != nullptr) {
                    w->_right->_col = BLACK;
                }
                rotateLeft(x->_parent);
                x = _root;  // 结束循环
            }
        } else {
            // x 是右孩子(对称情况)
            Node* w = x->_parent->_left;
            
            if (w != nullptr && w->_col == RED) {
                w->_col = BLACK;
                x->_parent->_col = RED;
                rotateRight(x->_parent);
                w = x->_parent->_left;
            }
            
            if ((w->_right == nullptr || w->_right->_col == BLACK) &&
                (w->_left == nullptr || w->_left->_col == BLACK)) {
                if (w != nullptr) {
                    w->_col = RED;
                }
                x = x->_parent;
            } else {
                if (w->_left == nullptr || w->_left->_col == BLACK) {
                    if (w->_right != nullptr) {
                        w->_right->_col = BLACK;
                    }
                    w->_col = RED;
                    rotateLeft(w);
                    w = x->_parent->_left;
                }
                w->_col = x->_parent->_col;
                x->_parent->_col = BLACK;
                if (w->_left != nullptr) {
                    w->_left->_col = BLACK;
                }
                rotateRight(x->_parent);
                x = _root;
            }
        }
    }
    
    if (x != nullptr) {
        x->_col = BLACK;
    }
}

8.5 删除操作的复杂度

  • 时间复杂度:\(O(\log n)\),修复过程中最多进行 \(O(\log n)\) 次颜色调整,且最多进行3次旋转。
  • 空间复杂度:\(O(1)\)。

9. 红黑树的查找操作

红黑树的查找操作与普通二叉搜索树完全相同,不涉及任何颜色或平衡调整。

Node* find(const K& key) {
    Node* cur = _root;
    while (cur != nullptr) {
        if (key < cur->_kv.first) {
            cur = cur->_left;
        } else if (key > cur->_kv.first) {
            cur = cur->_right;
        } else {
            return cur;
        }
    }
    return nullptr;  // 未找到
}

时间复杂度:\(O(\log n)\),由红黑树的高度上界保证。

10. 红黑树与2-3-4树的等价性

红黑树与2-3-4树(即4阶B树)具有等价性。这一等价性有助于从另一个角度理解红黑树的平衡机制。

10.1 2-3-4树简介

2-3-4树是一种平衡多路搜索树,每个节点可以包含1、2或3个关键字,相应地有2、3或4个子节点:

  • 2-节点:1个关键字,2个子节点。
  • 3-节点:2个关键字,3个子节点。
  • 4-节点:3个关键字,4个子节点。

2-3-4树的所有叶子节点都在同一层,因此是完美平衡的。

10.2 等价变换规则

红黑树与2-3-4树的等价变换规则如下:

  1. 黑色节点与它的红色子节点融合在一起,形成一个B树节点(2-3-4树节点)。
  2. 在B树节点中,黑色节点永远是父节点(位于中间),红色节点位于两侧。
  3. 红黑树的黑色节点个数等于2-3-4树的节点总个数。

等价变换示例:

红黑树片段                    等价2-3-4树节点

    [B]                         [  B  R  ]
    / \                         /  |  |  \
  [R] [ ]                   (形成3-节点或4-节点)
  / \
  • 一个黑色节点带一个红色左孩子 → 2-3-4树中的3-节点。
  • 一个黑色节点带一个红色右孩子 → 2-3-4树中的3-节点。
  • 一个黑色节点带两个红色孩子(左和右)→ 2-3-4树中的4-节点。

10.3 等价性的意义

红黑树的插入和删除操作可以理解为2-3-4树中节点的分裂和合并操作:

  • 红黑树的颜色翻转(父节点变红、叔节点变黑)对应2-3-4树中4-节点的分裂。
  • 红黑树的旋转对应2-3-4树中节点的重组。

这种等价性为理解和证明红黑树的操作提供了另一种视角。

11. 红黑树与AVL树的对比

红黑树和AVL树是两种最著名的自平衡二叉搜索树,它们有着不同的设计哲学和适用场景。

11.1 AVL树简介

AVL树(以发明者Adelson-Velsky和Landis命名)是最早的自平衡二叉搜索树。AVL树通过在每个节点维护平衡因子(Balance Factor,左子树高度减右子树高度),并严格要求任何节点的平衡因子绝对值不超过1。

11.2 核心差异对比

特性 AVL树 红黑树
平衡策略 严格平衡(高度差≤1) 近似平衡(最长路径≤2×最短路径)
平衡维护方式 平衡因子 节点颜色
查找效率 更快(更严格平衡) 略慢但仍是\(O(\log n)\)
插入/删除效率 较多旋转 较少旋转
内存占用 需存储平衡因子 需存储颜色(通常1位即可)
适用场景 查找密集型 插入/删除密集型

11.3 详细分析

查找性能:AVL树由于更严格的平衡,树高更接近 \(\log_2 n\),因此查找操作略快于红黑树。

插入/删除性能:红黑树允许一定程度的不平衡,因此在插入和删除时需要的旋转次数更少。统计性能上,红黑树通常优于AVL树。

内存效率:红黑树只需1位存储颜色(可用一个bit实现),而AVL树需要存储平衡因子(通常需要额外的整数)。红黑树相比AVL树可节约约20%的内存。

实现复杂度:红黑树的插入和删除实现比AVL树更复杂,涉及多种情况分类。

11.4 选择建议

  • 查找操作远多于插入/删除:选择AVL树。
  • 插入/删除操作频繁:选择红黑树。
  • 需要有序迭代:两者都可,但红黑树在C++ STL中已被广泛采用。

12. C++中红黑树的实现

12.1 完整的红黑树类模板

以下是红黑树的完整C++实现框架:

#include <iostream>
#include <utility>

enum Color { RED, BLACK };

template<typename K, typename V>
class RBTree {
private:
    struct Node {
        std::pair<K, V> _kv;
        Node* _left;
        Node* _right;
        Node* _parent;
        Color _col;
        
        Node(const std::pair<K, V>& kv)
            : _kv(kv)
            , _left(nullptr)
            , _right(nullptr)
            , _parent(nullptr)
            , _col(RED) {}
    };
    
    Node* _root;
    
    // 私有方法:左旋、右旋、插入修复、删除修复等
    void rotateLeft(Node* x);
    void rotateRight(Node* y);
    void fixAfterInsertion(Node* z);
    void fixAfterDeletion(Node* x);
    void transplant(Node* u, Node* v);
    Node* minimum(Node* x);
    
public:
    RBTree() : _root(nullptr) {}
    ~RBTree() { destroy(_root); }
    
    void insert(const std::pair<K, V>& kv);
    void erase(const K& key);
    Node* find(const K& key);
    void inorderTraversal();
    
private:
    void destroy(Node* node);
    void inorder(Node* node);
};

12.2 迭代器设计

在C++ STL中,红黑树需要支持迭代器(Iterator)以便遍历容器中的元素。迭代器通常设计为双向迭代器(Bidirectional Iterator),支持 ++ 和 -- 操作。

迭代器的核心是前驱(Predecessor)和后继(Successor)查找算法:

// 查找中序后继
Node* successor(Node* x) {
    if (x->_right != nullptr) {
        // 如果有右子树,后继是右子树中的最小节点
        Node* cur = x->_right;
        while (cur->_left != nullptr) {
            cur = cur->_left;
        }
        return cur;
    }
    
    // 否则向上查找
    Node* cur = x;
    Node* parent = cur->_parent;
    while (parent != nullptr && cur == parent->_right) {
        cur = parent;
        parent = parent->_parent;
    }
    return parent;
}

// 查找中序前驱
Node* predecessor(Node* x) {
    if (x->_left != nullptr) {
        Node* cur = x->_left;
        while (cur->_right != nullptr) {
            cur = cur->_right;
        }
        return cur;
    }
    
    Node* cur = x;
    Node* parent = cur->_parent;
    while (parent != nullptr && cur == parent->_left) {
        cur = parent;
        parent = parent->_parent;
    }
    return parent;
}

12.3 哨兵节点的使用

在实际实现中,通常使用一个全局的哨兵节点(Sentinel Node)NIL 来代表所有叶子节点。哨兵节点是黑色的,不存储任何实际数据。

使用哨兵节点的好处:

  1. 简化边界条件的处理(无需检查空指针)。
  2. 统一所有叶子节点的颜色(黑色)。
  3. 便于实现迭代器(end() 可以指向哨兵节点)。

13. STL中map与set的底层实现

13.1 STL中的红黑树封装

在C++ STL(SGI版本)中,std::map 和 std::set 的底层都依赖于红黑树实现。源代码主要分布在三个头文件中:

  • stl_tree.h:红黑树的通用实现。
  • stl_map.h:std::map 的封装。
  • stl_set.h:std::set 的封装。

13.2 红黑树的模板参数

红黑树类模板的核心参数如下:

template <class Key, class Value, class KeyOfValue, class Compare, class Alloc = alloc>
class rb_tree {
    // Key: 键的类型
    // Value: 存储的值的类型(对set是Key,对map是pair<const Key, T>)
    // KeyOfValue: 从Value中提取Key的函数对象
    // Compare: 键的比较函数
    // Alloc: 分配器
};

13.3 set的封装

std::set 存储的是键的集合,键值唯一且有序:

template <class Key, class Compare = less<Key>, class Alloc = alloc>
class set {
public:
    typedef Key key_type;
    typedef Key value_type;  // set的value_type就是key本身
    
private:
    // 红黑树类型定义
    typedef rb_tree<key_type, value_type, 
                    identity<value_type>, key_compare, Alloc> rep_type;
    rep_type t;  // 红黑树对象
};

13.4 map的封装

std::map 存储的是键值对的映射,键值唯一且按键有序:

template <class Key, class T, class Compare = less<Key>, class Alloc = alloc>
class map {
public:
    typedef Key key_type;
    typedef T mapped_type;
    typedef pair<const Key, T> value_type;  // map存储的是键值对
    
private:
    typedef rb_tree<key_type, value_type,
                    select1st<value_type>, key_compare, Alloc> rep_type;
    rep_type t;  // 红黑树对象
};

13.5 核心设计思想

STL中红黑树的设计体现了泛型编程(Generic Programming)的核心思想:

  1. 单一实现,多种用途:同一棵红黑树可以同时支持 set(键集合)和 map(键值映射)。
  2. 通过模板参数区分:KeyOfValue 参数决定如何从存储的值中提取键。
  3. 迭代器统一:红黑树提供双向迭代器,set 和 map 直接复用。

13.6 复杂度保证

C++标准要求 std::map 和 std::set 的查找、插入、删除操作具有对数复杂度:

  • 查找 find():\(O(\log n)\)
  • 插入 insert():\(O(\log n)\)
  • 删除 erase():\(O(\log n)\)
  • 迭代:\(O(n)\)(中序遍历)

14. 红黑树的应用场景

红黑树因其优秀的平衡性能和相对较低的维护成本,在众多领域得到广泛应用。

14.1 C++ STL关联容器

C++标准库中的 std::map、std::set、std::multimap、std::multiset 均以红黑树为底层实现。

14.2 Java集合框架

Java中的 TreeMap 和 TreeSet 同样基于红黑树实现。

14.3 Linux内核

Linux内核中大量使用红黑树:

  • CFQ I/O调度器:使用红黑树跟踪I/O请求。
  • 完全公平调度器(CFS):使用红黑树管理进程。
  • 高精度定时器(hrtimer):使用红黑树组织未完成的定时器请求。
  • ext3/ext4文件系统:使用红黑树跟踪目录条目。
  • 内存管理:使用红黑树维护内存块的映射(如 vm_area_struct)。
  • 网络数据包管理:使用红黑树进行快速查找和组织。

14.4 数据库系统

数据库系统中,红黑树常用于:

  • 索引管理:维护有序索引结构。
  • 查询优化:组织查询执行计划。
  • 缓存管理:实现LRU缓存等。

14.5 其他应用

  • 计算几何:用于区间树、线段树等几何数据结构的实现。
  • 事件模拟:用于维护按时间排序的事件队列。
  • 编译器:用于符号表的实现。

15. 总结

红黑树是一种自平衡二叉搜索树,通过五个红黑性质的约束,确保树的高度始终为 \(O(\log n)\),从而保证查找、插入、删除等操作在最坏情况下的时间复杂度为 \(O(\log n)\)。

15.1 核心要点回顾

  1. 五大性质:节点颜色(红/黑)、根黑、叶黑、无连续红、黑高一致。

  2. 黑高度与树高:黑高度 \(bh \geq h/2\),树高 \(h \leq 2\log_2(n+1)\)。

  3. 旋转操作:左旋和右旋是维护平衡的基本操作,时间复杂度 \(O(1)\)。

  4. 插入修复:分为三种情况,最多进行2次旋转。

  5. 删除修复:分为四种情况,最多进行3次旋转。

  6. 等价性:红黑树与2-3-4树等价。

  7. 与AVL对比:红黑树插入/删除更快,AVL树查找更快。

  8. C++实现:std::map 和 std::set 底层基于红黑树。

15.2 红黑树与AVL树的选择建议

场景 推荐
查找操作远多于插入/删除 AVL树
插入/删除操作频繁 红黑树
需要有序关联容器 红黑树(STL已实现)
内存受限 红黑树(节省约20%内存)

15.3 最终结论

红黑树通过在平衡性和维护开销之间取得精妙的平衡,成为了计算机科学中最成功和广泛使用的数据结构之一。它不仅是C++ STL关联容器的基石,也是操作系统内核、数据库系统等基础软件的重要组成部分。

理解红黑树的原理和实现,对于深入掌握数据结构和算法、编写高性能的C++程序具有重要意义。

posted @ 2026-09-20 17:17  Aries2011  阅读(53)  评论(0)    收藏  举报