引言
在计算机科学中,数据结构是程序的灵魂。而在众多数据结构中,红黑树 (Red-Black Tree) 无疑是最经典、应用最广泛的自平衡二叉搜索树之一。它由 Rudolf Bayer 于 1972 年发明,当时被称为 "对称二叉 B 树",后来在 1978 年由 Leonidas J. Guibas 和 Robert Sedgewick 修改为现在的红黑树形式。
红黑树通过在每个节点上增加一个颜色属性(红色或黑色),并遵循一系列严格的规则,保证了树的大致平衡。这使得红黑树在最坏情况下,插入、删除和查找操作的时间复杂度都能保持在O(log n),其中 n 是树中节点的数量。相比普通二叉搜索树在最坏情况下退化为链表(时间复杂度 O (n)),红黑树的性能优势非常明显。
一、红黑树的定义与基本性质
红黑树本质上是一棵二叉搜索树,它在二叉搜索树的基础上增加了以下 5 条性质:
- 节点颜色性质:每个节点要么是红色,要么是黑色。
- 根节点性质:根节点必须是黑色。
- 叶子节点性质:所有叶子节点(NIL 节点,即空节点)都是黑色。
- 红色节点性质:如果一个节点是红色的,那么它的两个子节点都是黑色的。(换句话说,不存在两个连续的红色节点)
- 黑高性质:从任意一个节点到其所有后代叶子节点的简单路径上,包含相同数量的黑色节点。
重要概念:黑高 (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) 次 |
| 维护开销 | 较低 | 较高 |
| 查找性能 | 较好 | 更好(因为树更矮) |
| 插入 / 删除性能 | 更好 | 较差 |
| 适用场景 | 插入删除频繁的场景 | 查找频繁、插入删除较少的场景 |
总的来说,红黑树在插入删除性能和查找性能之间取得了很好的平衡,因此在实际应用中更为广泛。
-
按照二叉搜索树的规则插入新节点,并将新节点颜色设为红色
-
检查并修复红黑树的性质
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 删除操作实现
红黑树的删除操作比插入操作更复杂。它也分为两步:
-
按照二叉搜索树的规则删除节点
-
检查并修复红黑树的性质
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
cppint 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)。
六、红黑树的实际应用
红黑树在计算机科学中有着极其广泛的应用,主要包括:
-
编程语言标准库
- C++ STL 中的
map、set、multimap和multiset - Java 中的
TreeMap和TreeSet - Linux 内核中的进程调度器(CFS 调度器使用红黑树管理进程)
- C++ STL 中的
-
数据库系统
- 用于实现索引结构,如 B + 树的变种
- 用于事务管理中的锁机制
-
操作系统
- Linux 内核中的内存管理(如虚拟内存区域的管理)
- 文件系统中的目录结构管理
-
其他应用
- 编译器中的符号表
- 网络路由器中的路由表
- 游戏中的碰撞检测