数据结构 --- 红黑树

引言

在计算机科学中,数据结构是程序的灵魂。而在众多数据结构中,红黑树 (Red-Black Tree) 无疑是最经典、应用最广泛的自平衡二叉搜索树之一。它由 Rudolf Bayer 于 1972 年发明,当时被称为 "对称二叉 B 树",后来在 1978 年由 Leonidas J. Guibas 和 Robert Sedgewick 修改为现在的红黑树形式。

红黑树通过在每个节点上增加一个颜色属性(红色或黑色),并遵循一系列严格的规则,保证了树的大致平衡。这使得红黑树在最坏情况下,插入、删除和查找操作的时间复杂度都能保持在O(log n),其中 n 是树中节点的数量。相比普通二叉搜索树在最坏情况下退化为链表(时间复杂度 O (n)),红黑树的性能优势非常明显。

一、红黑树的定义与基本性质

红黑树本质上是一棵二叉搜索树,它在二叉搜索树的基础上增加了以下 5 条性质:

  1. 节点颜色性质:每个节点要么是红色,要么是黑色。
  2. 根节点性质:根节点必须是黑色。
  3. 叶子节点性质:所有叶子节点(NIL 节点,即空节点)都是黑色。
  4. 红色节点性质:如果一个节点是红色的,那么它的两个子节点都是黑色的。(换句话说,不存在两个连续的红色节点)
  5. 黑高性质:从任意一个节点到其所有后代叶子节点的简单路径上,包含相同数量的黑色节点。

重要概念:黑高 (Black Height) 从某个节点 x 出发(不包括 x 本身)到达一个叶子节点的任意一条路径上,黑色节点的个数称为该节点的黑高,记为 bh (x)。根据性质 5,所有从 x 出发到叶子节点的路径黑高都相同。整棵红黑树的黑高就是根节点的黑高。

为什么这些性质能保证树的平衡? 性质 4 和性质 5 共同保证了:从根节点到任意叶子节点的最长可能路径长度,不会超过最短可能路径长度的两倍。因为最短路径全是黑色节点,最长路径是红黑交替的节点。而根据性质 4,红色节点不能连续,所以最长路径的长度最多是最短路径的两倍。这就保证了树的大致平衡,从而保证了操作的时间复杂度。

二、红黑树的完整 C++ 实现

2.1 节点结构与枚举定义

cpp 复制代码
#include <iostream>
#include <string>

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

// 红黑树节点结构
template <typename T>
struct RBTreeNode {
    T key;                // 关键字
    Color color;          // 节点颜色
    RBTreeNode* left;     // 左孩子指针
    RBTreeNode* right;    // 右孩子指针
    RBTreeNode* parent;   // 父节点指针

    // 构造函数
    RBTreeNode(const T& val) 
        : key(val), color(RED), left(nullptr), right(nullptr), parent(nullptr) {}
};

注意:新插入的节点默认是红色的。这是因为插入红色节点不会改变黑高,从而不会违反性质 5,只可能违反性质 4(如果父节点也是红色的)。而如果插入黑色节点,必然会改变黑高,违反性质 5,修复起来会更复杂。

2.2 红黑树类定义

cpp 复制代码
template <typename T>
class RBTree {
private:
    RBTreeNode<T>* root;  // 根节点指针
    RBTreeNode<T>* NIL;   // 哨兵节点(代表所有叶子节点)

    // 私有辅助函数
    void leftRotate(RBTreeNode<T>* x);    // 左旋操作
    void rightRotate(RBTreeNode<T>* x);   // 右旋操作
    void insertFixup(RBTreeNode<T>* z);   // 插入后修复红黑树性质
    void transplant(RBTreeNode<T>* u, RBTreeNode<T>* v);  // 替换子树
    void deleteFixup(RBTreeNode<T>* x);   // 删除后修复红黑树性质
    void destroyTree(RBTreeNode<T>* node); // 销毁树(用于析构函数)
    void inorderTraversalHelper(RBTreeNode<T>* node); // 中序遍历辅助函数

public:
    // 构造函数
    RBTree() {
        NIL = new RBTreeNode<T>(T());
        NIL->color = BLACK;
        root = NIL;
    }

    // 析构函数
    ~RBTree() {
        destroyTree(root);
        delete NIL;
    }

    // 公共接口
    void insert(const T& key);            // 插入节点
    bool remove(const T& key);            // 删除节点
    RBTreeNode<T>* search(const T& key);  // 查找节点
    void inorderTraversal();              // 中序遍历(输出有序序列)
    RBTreeNode<T>* getRoot() { return root; } // 获取根节点
};

2.3 旋转操作实现

旋转是红黑树保持平衡的核心操作。当插入或删除节点导致红黑树的性质被破坏时,我们需要通过旋转来调整树的结构,然后再调整节点的颜色,以恢复红黑树的性质。

cpp 复制代码
// 左旋操作:将节点x绕其右孩子y逆时针旋转
template <typename T>
void RBTree<T>::leftRotate(RBTreeNode<T>* x) {
    RBTreeNode<T>* y = x->right;       // y是x的右孩子
    x->right = y->left;                // 将y的左子树变成x的右子树

    if (y->left != NIL) {
        y->left->parent = x;           // 如果y的左子树非空,更新其父节点
    }

    y->parent = x->parent;             // 将x的父节点链接到y

    if (x->parent == NIL) {
        root = y;                      // 如果x是根节点,更新根节点为y
    } else if (x == x->parent->left) {
        x->parent->left = y;           // 如果x是左孩子,将y设为x父节点的左孩子
    } else {
        x->parent->right = y;          // 如果x是右孩子,将y设为x父节点的右孩子
    }

    y->left = x;                       // 将x设为y的左孩子
    x->parent = y;                     // 更新x的父节点为y
}

// 右旋操作:将节点x绕其左孩子y顺时针旋转
template <typename T>
void RBTree<T>::rightRotate(RBTreeNode<T>* x) {
    RBTreeNode<T>* y = x->left;        // y是x的左孩子
    x->left = y->right;                // 将y的右子树变成x的左子树

    if (y->right != NIL) {
        y->right->parent = x;          // 如果y的右子树非空,更新其父节点
    }

    y->parent = x->parent;             // 将x的父节点链接到y

    if (x->parent == NIL) {
        root = y;                      // 如果x是根节点,更新根节点为y
    } else if (x == x->parent->right) {
        x->parent->right = y;          // 如果x是右孩子,将y设为x父节点的右孩子
    } else {
        x->parent->left = y;           // 如果x是左孩子,将y设为x父节点的左孩子
    }

    y->right = x;                      // 将x设为y的右孩子
    x->parent = y;                     // 更新x的父节点为y
}

重要性质:旋转操作不会改变二叉搜索树的性质,即旋转后,树仍然满足 "左子树所有节点关键字小于根节点关键字,右子树所有节点关键字大于根节点关键字" 的性质。

2.4 插入操作实现

红黑树的插入操作分为两步:

八、总结

红黑树是一种非常优秀的自平衡二叉搜索树,它通过简单的颜色规则和旋转操作,保证了在最坏情况下所有基本操作的时间复杂度都是 O (log n)。红黑树的实现虽然有一定的复杂度,但它的性能优势和广泛的应用场景,使得它成为每个计算机专业学生和开发者必须掌握的数据结构之一。

理解红黑树的关键在于理解它的 5 条性质,以及插入和删除操作后如何通过旋转和颜色调整来恢复这些性质。虽然红黑树的细节比较繁琐,但只要掌握了基本思想和核心操作,就能很好地理解和应用它。

七、红黑树与其他平衡树的比较

红黑树并不是唯一的自平衡二叉搜索树,其他常见的还有 AVL 树、Splay 树、Treap 等。下面是红黑树与 AVL 树的比较:

表格

特性 红黑树 AVL 树
平衡条件 相对宽松,最长路径不超过最短路径的 2 倍 严格平衡,左右子树高度差不超过 1
旋转次数 插入最多 2 次,删除最多 3 次 插入最多 2 次,删除最多 O (log n) 次
维护开销 较低 较高
查找性能 较好 更好(因为树更矮)
插入 / 删除性能 更好 较差
适用场景 插入删除频繁的场景 查找频繁、插入删除较少的场景

总的来说,红黑树在插入删除性能和查找性能之间取得了很好的平衡,因此在实际应用中更为广泛。

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

  2. 检查并修复红黑树的性质

    cpp 复制代码
    // 插入节点
    template <typename T>
    void RBTree<T>::insert(const T& key) {
        RBTreeNode<T>* z = new RBTreeNode<T>(key);
        RBTreeNode<T>* y = NIL;
        RBTreeNode<T>* x = root;
    
        // 按照二叉搜索树的规则找到插入位置
        while (x != NIL) {
            y = x;
            if (z->key < x->key) {
                x = x->left;
            } else {
                x = x->right;
            }
        }
    
        z->parent = y;
        if (y == NIL) {
            root = z;                      // 如果树为空,z成为根节点
        } else if (z->key < y->key) {
            y->left = z;                   // z是左孩子
        } else {
            y->right = z;                  // z是右孩子
        }
    
        // 初始化新节点的左右孩子为哨兵节点
        z->left = NIL;
        z->right = NIL;
        z->color = RED;                    // 新节点默认红色
    
        // 修复红黑树性质
        insertFixup(z);
    }
    
    // 插入后修复红黑树性质
    template <typename T>
    void RBTree<T>::insertFixup(RBTreeNode<T>* z) {
        while (z->parent->color == RED) {
            if (z->parent == z->parent->parent->left) {
                RBTreeNode<T>* y = z->parent->parent->right; // 叔叔节点
    
                // 情况1:叔叔节点是红色的
                if (y->color == RED) {
                    z->parent->color = BLACK;
                    y->color = BLACK;
                    z->parent->parent->color = RED;
                    z = z->parent->parent; // 向上继续检查
                } else {
                    // 情况2:叔叔节点是黑色的,且z是右孩子
                    if (z == z->parent->right) {
                        z = z->parent;
                        leftRotate(z);     // 左旋转化为情况3
                    }
    
                    // 情况3:叔叔节点是黑色的,且z是左孩子
                    z->parent->color = BLACK;
                    z->parent->parent->color = RED;
                    rightRotate(z->parent->parent);
                }
            } else {
                // 对称情况:父节点是祖父节点的右孩子
                RBTreeNode<T>* y = z->parent->parent->left; // 叔叔节点
    
                // 情况1:叔叔节点是红色的
                if (y->color == RED) {
                    z->parent->color = BLACK;
                    y->color = BLACK;
                    z->parent->parent->color = RED;
                    z = z->parent->parent; // 向上继续检查
                } else {
                    // 情况2:叔叔节点是黑色的,且z是左孩子
                    if (z == z->parent->left) {
                        z = z->parent;
                        rightRotate(z);    // 右旋转化为情况3
                    }
    
                    // 情况3:叔叔节点是黑色的,且z是右孩子
                    z->parent->color = BLACK;
                    z->parent->parent->color = RED;
                    leftRotate(z->parent->parent);
                }
            }
        }
    
        root->color = BLACK; // 确保根节点始终是黑色的
    }

    2.5 删除操作实现

    红黑树的删除操作比插入操作更复杂。它也分为两步:

  3. 按照二叉搜索树的规则删除节点

  4. 检查并修复红黑树的性质

    cpp 复制代码
    // 替换子树:用v替换u作为子树的根
    template <typename T>
    void RBTree<T>::transplant(RBTreeNode<T>* u, RBTreeNode<T>* v) {
        if (u->parent == NIL) {
            root = v;
        } else if (u == u->parent->left) {
            u->parent->left = v;
        } else {
            u->parent->right = v;
        }
        v->parent = u->parent;
    }
    
    // 删除节点
    template <typename T>
    bool RBTree<T>::remove(const T& key) {
        RBTreeNode<T>* z = search(key);
        if (z == NIL) {
            return false; // 节点不存在,删除失败
        }
    
        RBTreeNode<T>* y = z;
        RBTreeNode<T>* x;
        Color yOriginalColor = y->color;
    
        if (z->left == NIL) {
            x = z->right;
            transplant(z, z->right);
        } else if (z->right == NIL) {
            x = z->left;
            transplant(z, z->left);
        } else {
            // 找到z的后继节点(右子树的最小节点)
            y = z->right;
            while (y->left != NIL) {
                y = y->left;
            }
            yOriginalColor = y->color;
            x = y->right;
    
            if (y->parent == z) {
                x->parent = y;
            } else {
                transplant(y, y->right);
                y->right = z->right;
                y->right->parent = y;
            }
    
            transplant(z, y);
            y->left = z->left;
            y->left->parent = y;
            y->color = z->color;
        }
    
        delete z;
    
        // 如果删除的是黑色节点,需要修复红黑树性质
        if (yOriginalColor == BLACK) {
            deleteFixup(x);
        }
    
        return true;
    }
    
    // 删除后修复红黑树性质
    template <typename T>
    void RBTree<T>::deleteFixup(RBTreeNode<T>* x) {
        while (x != root && x->color == BLACK) {
            if (x == x->parent->left) {
                RBTreeNode<T>* w = x->parent->right; // 兄弟节点
    
                // 情况1:兄弟节点是红色的
                if (w->color == RED) {
                    w->color = BLACK;
                    x->parent->color = RED;
                    leftRotate(x->parent);
                    w = x->parent->right;
                }
    
                // 情况2:兄弟节点是黑色的,且兄弟的两个孩子都是黑色的
                if (w->left->color == BLACK && w->right->color == BLACK) {
                    w->color = RED;
                    x = x->parent;
                } else {
                    // 情况3:兄弟节点是黑色的,兄弟的左孩子是红色,右孩子是黑色
                    if (w->right->color == BLACK) {
                        w->left->color = BLACK;
                        w->color = RED;
                        rightRotate(w);
                        w = x->parent->right;
                    }
    
                    // 情况4:兄弟节点是黑色的,兄弟的右孩子是红色
                    w->color = x->parent->color;
                    x->parent->color = BLACK;
                    w->right->color = BLACK;
                    leftRotate(x->parent);
                    x = root; // 结束循环
                }
            } else {
                // 对称情况:x是父节点的右孩子
                RBTreeNode<T>* w = x->parent->left; // 兄弟节点
    
                // 情况1:兄弟节点是红色的
                if (w->color == RED) {
                    w->color = BLACK;
                    x->parent->color = RED;
                    rightRotate(x->parent);
                    w = x->parent->left;
                }
    
                // 情况2:兄弟节点是黑色的,且兄弟的两个孩子都是黑色的
                if (w->right->color == BLACK && w->left->color == BLACK) {
                    w->color = RED;
                    x = x->parent;
                } else {
                    // 情况3:兄弟节点是黑色的,兄弟的右孩子是红色,左孩子是黑色
                    if (w->left->color == BLACK) {
                        w->right->color = BLACK;
                        w->color = RED;
                        leftRotate(w);
                        w = x->parent->left;
                    }
    
                    // 情况4:兄弟节点是黑色的,兄弟的左孩子是红色
                    w->color = x->parent->color;
                    x->parent->color = BLACK;
                    w->left->color = BLACK;
                    rightRotate(x->parent);
                    x = root; // 结束循环
                }
            }
        }
    
        x->color = BLACK;
    }

    2.6 查找与遍历操作实现

    cpp 复制代码
    // 查找节点
    template <typename T>
    RBTreeNode<T>* RBTree<T>::search(const T& key) {
        RBTreeNode<T>* x = root;
        while (x != NIL && key != x->key) {
            if (key < x->key) {
                x = x->left;
            } else {
                x = x->right;
            }
        }
        return x;
    }
    
    // 中序遍历辅助函数
    template <typename T>
    void RBTree<T>::inorderTraversalHelper(RBTreeNode<T>* node) {
        if (node != NIL) {
            inorderTraversalHelper(node->left);
            std::cout << node->key << "(" << (node->color == RED ? "红" : "黑") << ") ";
            inorderTraversalHelper(node->right);
        }
    }
    
    // 中序遍历(输出有序序列)
    template <typename T>
    void RBTree<T>::inorderTraversal() {
        inorderTraversalHelper(root);
        std::cout << std::endl;
    }
    
    // 销毁树(用于析构函数)
    template <typename T>
    void RBTree<T>::destroyTree(RBTreeNode<T>* node) {
        if (node != NIL) {
            destroyTree(node->left);
            destroyTree(node->right);
            delete node;
        }
    }

    三、使用示例与测试

    cpp

    cpp 复制代码
    int main() {
        RBTree<int> tree;
    
        // 插入测试
        std::cout << "插入节点:10, 20, 30, 15, 25, 5, 3" << std::endl;
        int keys[] = {10, 20, 30, 15, 25, 5, 3};
        for (int key : keys) {
            tree.insert(key);
        }
    
        std::cout << "中序遍历结果(有序):" << std::endl;
        tree.inorderTraversal();
    
        // 查找测试
        int searchKey = 15;
        RBTreeNode<int>* found = tree.search(searchKey);
        if (found != tree.getRoot()->parent) { // NIL节点的parent是自己
            std::cout << "找到节点 " << searchKey << ",颜色为" 
                      << (found->color == RED ? "红色" : "黑色") << std::endl;
        } else {
            std::cout << "未找到节点 " << searchKey << std::endl;
        }
    
        // 删除测试
        int deleteKey = 20;
        std::cout << "删除节点 " << deleteKey << std::endl;
        if (tree.remove(deleteKey)) {
            std::cout << "删除成功" << std::endl;
        } else {
            std::cout << "删除失败,节点不存在" << std::endl;
        }
    
        std::cout << "删除后的中序遍历结果:" << std::endl;
        tree.inorderTraversal();
    
        return 0;
    }

    四、程序运行结果plaintext

    bash 复制代码
    插入节点:10, 20, 30, 15, 25, 5, 3
    中序遍历结果(有序):
    3(红) 5(黑) 10(红) 15(黑) 20(黑) 25(红) 30(黑) 
    找到节点 15,颜色为黑色
    删除节点 20
    删除成功
    删除后的中序遍历结果:
    3(红) 5(黑) 10(红) 15(黑) 25(黑) 30(红) 

    五、红黑树的时间复杂度分析

    红黑树的高度 h 满足:h ≤ 2log₂(n+1)

    证明: 设红黑树的黑高为 bh。根据性质 5,从根节点到叶子节点的最短路径长度为 bh。根据性质 4,最长路径长度最多为 2bh(红黑交替)。因此,树的高度 h ≤ 2bh。

    另一方面,一棵黑高为 bh 的红黑树,至少包含 2^bh - 1 个节点(完全二叉树的情况)。因此: n ≥ 2^bh - 1 bh ≤ log₂(n+1)

    结合以上两个不等式: h ≤ 2log₂(n+1)

    这证明了红黑树的高度是 O (log n)。因此,红黑树的插入、删除和查找操作的时间复杂度都是 O (log n)。

    六、红黑树的实际应用

    红黑树在计算机科学中有着极其广泛的应用,主要包括:

  5. 编程语言标准库

    • C++ STL 中的mapsetmultimapmultiset
    • Java 中的TreeMapTreeSet
    • Linux 内核中的进程调度器(CFS 调度器使用红黑树管理进程)
  6. 数据库系统

    • 用于实现索引结构,如 B + 树的变种
    • 用于事务管理中的锁机制
  7. 操作系统

    • Linux 内核中的内存管理(如虚拟内存区域的管理)
    • 文件系统中的目录结构管理
  8. 其他应用

    • 编译器中的符号表
    • 网络路由器中的路由表
    • 游戏中的碰撞检测
相关推荐
Zhang~Ling4 小时前
C++ 红黑树封装:myset和mymap的底层实现
开发语言·数据结构·c++·算法
啦啦啦啦啦zzzz4 小时前
数据结构:堆排序
数据结构·c++·
San813_LDD4 小时前
[量化]《虚函数调用时间复杂度完全解析:为什么是 O(1) 以及它的真实代价》
java·数据结构·算法
起个破名想半天了5 小时前
算法与数据结构之Floyd算法
数据结构·算法
小七在进步5 小时前
数据结构:线性表之顺序表
c语言·数据结构·算法
Never_love_MCI!5 小时前
洛谷P15799 [GESP202603 五级] 找数 题解
数据结构·c++·算法
一只齐刘海的猫6 小时前
【Leetcode】三数之和
数据结构·算法·leetcode
lightqjx6 小时前
【算法】数据结构_扩展域并查集
数据结构·算法·并查集·扩展域并查集
San813_LDD6 小时前
[量化]《多线程数据同步精讲:std::mutex 的底层原理与最佳实践》
c语言·数据结构