红黑树
C++ 红黑树详解
目录
- 引言
- 红黑树的历史与定义
- 红黑树的五大性质
- 红黑树的高度与时间复杂度证明
- 红黑树的节点结构
- 旋转操作
- 红黑树的插入操作
- 红黑树的删除操作
- 红黑树的查找操作
- 红黑树与2-3-4树的等价性
- 红黑树与AVL树的对比
- C++中红黑树的实现
- STL中map与set的底层实现
- 红黑树的应用场景
- 总结
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\) 为根的子树至少包含:
个内部节点。
由归纳法,引理得证。
4.3 红黑树高度上界
定理 1:一棵含有 \(n\) 个内部节点的红黑树,其高度 \(h\) 至多为 \(2\log_2(n+1)\)。
证明:
设红黑树的高度为 \(h\),黑高度为 \(bh\)。根据性质4(不存在两个连续的红色节点),在从根到叶子的任何路径上,至少有一半的节点是黑色。因此:
由引理1,以根为根的子树(即整棵树)至少包含 \(2^{bh} - 1\) 个内部节点:
整理得:
两边取以2为底的对数:
定理得证。
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. 红黑树的插入操作
红黑树的插入操作分为两个阶段:
- 标准BST插入:按照二叉搜索树的规则插入新节点。
- 修复平衡:通过变色和旋转恢复红黑性质。
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):
- 将父节点 \(p\) 设为黑色。
- 将叔叔节点 \(u\) 设为黑色。
- 将祖父节点 \(g\) 设为红色(除非 \(g\) 是根节点)。
- 将 \(g\) 作为新的当前节点,向上回溯继续检查。
[g(B)] [g(R)]
/ \ ====> / \
[p(R)] [u(R)] [p(B)] [u(B)]
/ /
[z(R)] [z(R)]
情况2:叔叔节点是黑色,且当前节点是父节点的右孩子("LR"型)
这种情况需要先进行左旋,将"LR"型转化为"LL"型:
- 以父节点 \(p\) 为支点进行左旋。
- 旋转后,原来的当前节点 \(z\) 成为新的父节点,原来的父节点 \(p\) 成为 \(z\) 的左孩子。
- 进入情况3处理。
[g(B)] [g(B)]
/ /
[p(R)] ====> [z(R)]
\ /
[z(R)] [p(R)]
情况3:叔叔节点是黑色,且当前节点是父节点的左孩子("LL"型)
这是可以直接修复的情况:
- 将父节点 \(p\) 设为黑色。
- 将祖父节点 \(g\) 设为红色。
- 以祖父节点 \(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删除
红黑树的删除首先按照二叉搜索树的删除规则进行:
- 情况A:待删除节点 \(z\) 没有孩子 → 直接删除。
- 情况B:待删除节点 \(z\) 只有一个孩子 → 用孩子替换 \(z\)。
- 情况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\) 是红色
- 将兄弟节点 \(w\) 设为黑色。
- 将父节点 \(p\) 设为红色。
- 以父节点 \(p\) 为支点进行左旋(假设 \(x\) 是左孩子)。
- 更新兄弟节点 \(w\) 为 \(p\) 的新右孩子。
- 转化为情况2、3或4处理。
[p(B)] [w(B)]
/ \ ====> / \
[x(BB)] [w(R)] [p(R)] [γ]
/ \ / \
[α] [γ] [x(BB)] [α]
情况2:兄弟节点 \(w\) 是黑色,且 \(w\) 的两个孩子都是黑色
- 将兄弟节点 \(w\) 设为红色。
- 将 \(x\) 的"双重黑色"中的一重黑色上移到父节点 \(p\)。
- 如果 \(p\) 原来是红色,将 \(p\) 设为黑色,结束;如果 \(p\) 原来是黑色,则 \(p\) 成为新的"双重黑色"节点,向上回溯。
[p(任意)] [p(任意+1黑)]
/ \ ====> / \
[x(BB)] [w(B)] [x(B)] [w(R)]
/ \ / \
[α] [γ] [α] [γ]
情况3:兄弟节点 \(w\) 是黑色,\(w\) 的左孩子是红色,右孩子是黑色
- 将 \(w\) 的左孩子设为黑色。
- 将 \(w\) 设为红色。
- 以 \(w\) 为支点进行右旋。
- 更新兄弟节点 \(w\) 为 \(p\) 的新右孩子。
- 转化为情况4处理。
[p] [p]
/ /
[x(BB)] [w(B)] ====> [x(BB)] [α(R)]
/ \ \
[α(R)] [γ] [w(B)]
\
[γ]
情况4:兄弟节点 \(w\) 是黑色,且 \(w\) 的右孩子是红色
这是可以直接修复的情况:
- 将 \(w\) 的颜色设为 \(p\) 的颜色。
- 将 \(p\) 设为黑色。
- 将 \(w\) 的右孩子设为黑色。
- 以 \(p\) 为支点进行左旋。
- 消除 \(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树的等价变换规则如下:
- 黑色节点与它的红色子节点融合在一起,形成一个B树节点(2-3-4树节点)。
- 在B树节点中,黑色节点永远是父节点(位于中间),红色节点位于两侧。
- 红黑树的黑色节点个数等于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 来代表所有叶子节点。哨兵节点是黑色的,不存储任何实际数据。
使用哨兵节点的好处:
- 简化边界条件的处理(无需检查空指针)。
- 统一所有叶子节点的颜色(黑色)。
- 便于实现迭代器(
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)的核心思想:
- 单一实现,多种用途:同一棵红黑树可以同时支持
set(键集合)和map(键值映射)。 - 通过模板参数区分:
KeyOfValue参数决定如何从存储的值中提取键。 - 迭代器统一:红黑树提供双向迭代器,
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 核心要点回顾
-
五大性质:节点颜色(红/黑)、根黑、叶黑、无连续红、黑高一致。
-
黑高度与树高:黑高度 \(bh \geq h/2\),树高 \(h \leq 2\log_2(n+1)\)。
-
旋转操作:左旋和右旋是维护平衡的基本操作,时间复杂度 \(O(1)\)。
-
插入修复:分为三种情况,最多进行2次旋转。
-
删除修复:分为四种情况,最多进行3次旋转。
-
等价性:红黑树与2-3-4树等价。
-
与AVL对比:红黑树插入/删除更快,AVL树查找更快。
-
C++实现:
std::map和std::set底层基于红黑树。
15.2 红黑树与AVL树的选择建议
| 场景 | 推荐 |
|---|---|
| 查找操作远多于插入/删除 | AVL树 |
| 插入/删除操作频繁 | 红黑树 |
| 需要有序关联容器 | 红黑树(STL已实现) |
| 内存受限 | 红黑树(节省约20%内存) |
15.3 最终结论
红黑树通过在平衡性和维护开销之间取得精妙的平衡,成为了计算机科学中最成功和广泛使用的数据结构之一。它不仅是C++ STL关联容器的基石,也是操作系统内核、数据库系统等基础软件的重要组成部分。
理解红黑树的原理和实现,对于深入掌握数据结构和算法、编写高性能的C++程序具有重要意义。
本文来自博客园,作者:Aries2011,转载请注明原文链接:https://www.cnblogs.com/Aries2011/p/23051481

浙公网安备 33010602011771号