一、红黑树概述:优雅的自平衡结构
红黑树是一种自平衡的二叉搜索树,它通过在节点上增加一个颜色标记(红或黑)并遵循特定的规则,来确保树保持大致平衡的状态。相比于AVL树,红黑树对平衡的要求更加宽松,但在实际应用中性能同样出色。
红黑树的五大核心规则
- 颜色规则:每个节点不是红色就是黑色
- 根规则:根节点必须是黑色
- 红色节点规则:红色节点的子节点必须是黑色(不能有连续的红色节点)
- 黑色节点规则:从任意节点到其所有空叶子节点(NIL)的路径上,黑色节点的数量必须相同
- 叶子规则:所有叶子节点(NIL)都是黑色(这一条常被隐含处理)
红黑树为什么能保证平衡?
红黑树的精妙之处在于它通过颜色约束间接控制了树的平衡:
- 最短路径:全是黑色节点(由规则4保证所有路径黑色节点数相同)
- 最长路径:红黑交替(由规则3保证不会出现连续红色节点)
假设最短路径长度为 bh(black height),那么:
- 最短路径长度 =
bh - 最长路径长度 ≤
2 × bh
因此,最长路径不会超过最短路径的两倍,这保证了树的近似平衡。
红黑树 vs AVL树
| 特性 | 红黑树 | AVL树 |
|---|---|---|
| 平衡标准 | 近似平衡 | 严格平衡 |
| 旋转频率 | 较少 | 较多 |
| 查找效率 | O(log N) | O(log N) |
| 插入/删除 | 旋转少,性能好 | 旋转多,性能稍差 |
| 适用场景 | 需要频繁插入删除 | 查找密集,插入删除少 |
二、红黑树的结构设计
节点结构
cpp
// 颜色枚举
enum Colour {
RED,
BLACK
};
// 红黑树节点
template<class K, class V>
struct RBTreeNode {
std::pair<K, V> _kv; // 键值对
RBTreeNode<K, V>* _left; // 左孩子
RBTreeNode<K, V>* _right; // 右孩子
RBTreeNode<K, V>* _parent; // 父节点(重要!用于回溯)
Colour _col; // 颜色
RBTreeNode(const std::pair<K, V>& kv)
: _kv(kv)
, _left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _col(RED) // 新节点默认红色(重要!)
{}
};
红黑树类框架
cpp
template<class K, class V>
class RBTree {
typedef RBTreeNode<K, V> Node;
public:
RBTree() : _root(nullptr) {}
// 基本操作
bool Insert(const std::pair<K, V>& kv);
Node* Find(const K& key);
bool IsBalance(); // 验证红黑树性质
// 旋转操作(与AVL树类似)
void RotateL(Node* parent); // 左旋
void RotateR(Node* parent); // 右旋
private:
Node* _root = nullptr;
};
三、红黑树的插入操作详解
红黑树的插入分为两大步:BST标准插入 + 颜色调整。
3.1 插入步骤概览
- 按BST规则插入:找到合适位置插入新节点
- 颜色处理 :
- 空树插入:新节点设为黑色
- 非空树插入:新节点设为红色(如果设为黑色会破坏规则4)
- 调整平衡 :
- 如果父节点是黑色:插入完成
- 如果父节点是红色:需要调整(违反规则3)
3.2 关键概念和符号
在讨论调整策略前,先定义几个重要节点:
- c (cur):当前节点(新插入的节点)
- p (parent):c的父节点
- g (grandfather):p的父节点
- u (uncle):p的兄弟节点
3.3 插入调整的三种情况
情况一:叔叔节点u存在且为红色
处理方式 :变色即可,不需要旋转
cpp
// p和u变黑,g变红
p->_col = BLACK;
u->_col = BLACK;
g->_col = RED;
// 将g作为新的c,继续向上调整
cur = grandfather;
parent = cur->_parent;
原理分析:
- p和u变黑:左右子树各增加一个黑节点
- g变红:保持子树黑色节点总数不变
- 继续向上处理:因为g变红后,可能与其父节点形成连续红色
情况二:叔叔节点u不存在或为黑色,且c是p的左孩子
处理方式 :右单旋 + 变色
cpp
// 以g为轴右旋
RotateR(g);
// 变色
p->_col = BLACK;
g->_col = RED;
图形表示:
g(B) p(B)
/ \ / \
p(R) u(B) ==> c(R) g(R)
/ \
c(R) u(B)
情况三:叔叔节点u不存在或为黑色,且c是p的右孩子
处理方式 :左右双旋 + 变色
cpp
// 先以p为轴左旋,再以g为轴右旋
RotateL(p);
RotateR(g);
// 变色
c->_col = BLACK;
g->_col = RED;
图形表示:
g(B) c(B)
/ \ / \
p(R) u(B) ==> p(R) g(R)
\ /
c(R) u(B)
3.4 完整插入代码实现
cpp
bool Insert(const std::pair<K, V>& kv) {
// 1. 空树插入
if (_root == nullptr) {
_root = new Node(kv);
_root->_col = BLACK; // 根节点必须是黑色
return true;
}
// 2. 非空树:BST插入
Node* parent = nullptr;
Node* cur = _root;
while (cur) {
if (cur->_kv.first < kv.first) {
parent = cur;
cur = cur->_right;
} else if (cur->_kv.first > kv.first) {
parent = cur;
cur = cur->_left;
} else {
return false; // 键已存在
}
}
// 创建新节点(默认红色)
cur = new Node(kv);
cur->_col = RED;
// 链接到父节点
if (parent->_kv.first < kv.first) {
parent->_right = cur;
} else {
parent->_left = cur;
}
cur->_parent = parent;
// 3. 调整颜色(父节点为红色时才需要调整)
while (parent && parent->_col == RED) {
Node* grandfather = parent->_parent;
// 情况一:父节点是祖父的左孩子
if (parent == grandfather->_left) {
Node* uncle = grandfather->_right;
// 情况1.1:叔叔存在且为红
if (uncle && uncle->_col == RED) {
// 变色处理
parent->_col = BLACK;
uncle->_col = BLACK;
grandfather->_col = RED;
// 继续向上调整
cur = grandfather;
parent = cur->_parent;
}
// 情况1.2:叔叔不存在或为黑
else {
// 情况1.2.1:c是p的左孩子(右单旋)
if (cur == parent->_left) {
RotateR(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
}
// 情况1.2.2:c是p的右孩子(左右双旋)
else {
RotateL(parent);
RotateR(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
break; // 旋转后调整完成
}
}
// 情况二:父节点是祖父的右孩子(对称处理)
else {
Node* uncle = grandfather->_left;
// 情况2.1:叔叔存在且为红
if (uncle && uncle->_col == RED) {
parent->_col = BLACK;
uncle->_col = BLACK;
grandfather->_col = RED;
cur = grandfather;
parent = cur->_parent;
}
// 情况2.2:叔叔不存在或为黑
else {
// 情况2.2.1:c是p的右孩子(左单旋)
if (cur == parent->_right) {
RotateL(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
}
// 情况2.2.2:c是p的左孩子(右左双旋)
else {
RotateR(parent);
RotateL(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
break; // 旋转后调整完成
}
}
}
// 保证根节点为黑色
_root->_col = BLACK;
return true;
}
四、旋转操作实现
红黑树的旋转与AVL树类似,但不需要更新平衡因子:
cpp
// 左旋(以parent为旋转点)
void RotateL(Node* parent) {
Node* subR = parent->_right;
Node* subRL = subR->_left;
// 1. subRL成为parent的右孩子
parent->_right = subRL;
if (subRL) {
subRL->_parent = parent;
}
// 2. 记录parent的父节点
Node* parentParent = parent->_parent;
// 3. parent成为subR的左孩子
subR->_left = parent;
parent->_parent = subR;
// 4. subR连接到原parent的父节点
if (parentParent == nullptr) {
_root = subR;
subR->_parent = nullptr;
} else {
if (parent == parentParent->_left) {
parentParent->_left = subR;
} else {
parentParent->_right = subR;
}
subR->_parent = parentParent;
}
}
// 右旋(对称操作)
void RotateR(Node* parent) {
Node* subL = parent->_left;
Node* subLR = subL->_right;
parent->_left = subLR;
if (subLR) {
subLR->_parent = parent;
}
Node* parentParent = parent->_parent;
subL->_right = parent;
parent->_parent = subL;
if (parentParent == nullptr) {
_root = subL;
subL->_parent = nullptr;
} else {
if (parent == parentParent->_left) {
parentParent->_left = subL;
} else {
parentParent->_right = subL;
}
subL->_parent = parentParent;
}
}
五、红黑树的验证
验证红黑树需要检查所有规则:
cpp
// 检查红黑树性质
bool IsBalance() {
// 空树是红黑树
if (_root == nullptr) {
return true;
}
// 规则2:根节点必须是黑色
if (_root->_col != BLACK) {
std::cout << "违反规则2:根节点不是黑色" << std::endl;
return false;
}
// 计算参考值:任意一条路径的黑色节点数
int refBlackNum = 0;
Node* cur = _root;
while (cur) {
if (cur->_col == BLACK) {
refBlackNum++;
}
cur = cur->_left; // 走最左路径
}
// 递归检查所有规则
return Check(_root, 0, refBlackNum);
}
// 递归检查辅助函数
bool Check(Node* root, int blackNum, int refBlackNum) {
if (root == nullptr) {
// 到达叶子,检查黑色节点数
if (blackNum != refBlackNum) {
std::cout << "违反规则4:路径黑色节点数不一致" << std::endl;
return false;
}
return true;
}
// 规则3:检查连续红色节点(通过检查父节点)
if (root->_col == RED && root->_parent && root->_parent->_col == RED) {
std::cout << "违反规则3:存在连续红色节点" << std::endl;
return false;
}
// 统计黑色节点数
if (root->_col == BLACK) {
blackNum++;
}
// 递归检查左右子树
return Check(root->_left, blackNum, refBlackNum) &&
Check(root->_right, blackNum, refBlackNum);
}
六、查找操作
红黑树的查找与普通BST相同:
cpp
Node* Find(const K& key) {
Node* cur = _root;
while (cur) {
if (cur->_kv.first < key) {
cur = cur->_right;
} else if (cur->_kv.first > key) {
cur = cur->_left;
} else {
return cur; // 找到
}
}
return nullptr; // 未找到
}
七、测试用例
cpp
#include <iostream>
#include <vector>
#include <cstdlib>
#include <ctime>
void TestRBTree() {
std::cout << "=== 红黑树测试 ===" << std::endl;
RBTree<int, int> tree;
// 测试用例1:插入测试
std::vector<int> nums = {16, 3, 7, 11, 9, 26, 18, 14, 15};
std::cout << "插入序列: ";
for (int num : nums) {
std::cout << num << " ";
tree.Insert({num, num * 10});
}
std::cout << std::endl;
std::cout << "红黑树是否平衡: " << tree.IsBalance() << std::endl;
// 测试用例2:查找测试
std::cout << "\n查找测试:" << std::endl;
for (int num : {7, 15, 100}) {
auto node = tree.Find(num);
if (node) {
std::cout << "找到键 " << num << ", 值 = " << node->_kv.second << std::endl;
} else {
std::cout << "未找到键 " << num << std::endl;
}
}
// 测试用例3:大量数据测试
std::cout << "\n大量数据测试:" << std::endl;
RBTree<int, int> largeTree;
std::srand(std::time(0));
const int N = 10000;
for (int i = 0; i < N; i++) {
int key = std::rand() % 100000;
largeTree.Insert({key, i});
}
std::cout << "插入" << N << "个随机键后,树是否平衡: "
<< largeTree.IsBalance() << std::endl;
}
int main() {
TestRBTree();
return 0;
}
八、总结
红黑树的核心思想
- 颜色标记:通过红黑颜色实现简单的状态表示
- 规则约束:四条简单规则保证树的平衡性
- 局部调整:插入/删除时只影响局部,通过变色和旋转修复
红黑树的优势
- 性能稳定:最坏情况也是O(log N)
- 维护成本低:比AVL树旋转次数少
- 内存友好:每个节点只多一个颜色位
- 应用广泛:C++ STL的map/set、Linux内核、Java的TreeMap等都使用红黑树
学习要点
- 理解规则:四条规则必须牢记
- 掌握情况分析:三种插入情况的处理方式
- 练习实现:手动实现加深理解
- 对比学习:与AVL树对比,理解不同平衡策略
红黑树是数据结构中的经典内容,虽然实现较为复杂,但理解其原理后,你会发现它的设计非常精妙,是平衡性能和实现复杂度的绝佳折中方案。
