《从理论到实践:红黑树的自平衡机制与C++高效实现指南》
前言:在计算机科学领域,数据结构的选择直接决定着算法性能的巅峰。红黑树——这一被誉为"最优雅的平衡二叉搜索树",凭借其严格的平衡约束和稳定的对数级时间复杂度(O(log n)),已成为高性能系统的核心支柱。从Linux内核的进程调度到C++ STL的map容器,从数据库引擎的B+树后备存储到实时系统的内存管理,红黑树的身影无处不在。
目录
一、红黑树的定义
二、红黑树的性质
三、红黑树实现的总体思路
四、红黑树的节点结构
五、红黑树的插入操作
1.按照二叉搜索的树规则插入新节点
2.检测新节点插入后,红黑树的性质是否造到破坏
情况一: cur为红,p为红,g为黑,u存在且为红
情况二: cur为红,p为红,g为黑,u不存在/u存在且为黑
情况三: cur为红,p为红,g为黑,u不存在/u存在且为黑
六、红黑树的验证
总代码
一、红黑树的定义
红黑树,是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或 Black。 通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路 径会比其他路径长出俩倍,因而是接近平衡的。
二、红黑树的性质
1.每个结点不是红色就是黑色
红黑树的每个节点都有颜色属性:红和黑。不允许出现其它颜色情况
2.根节点是黑色的
整棵红黑树的根节点必须是黑色
3.如果一个节点是红色的,则它的两个孩子结点是黑色的
也就是说,父子节点不可能出现连续的红色
4.每个叶子节点都是黑色的
红黑树的叶子节点通常定义为 NIL(空节点),即使实际树中不画,需视为黑色节点
5.对于每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点
简单路径:树中不重复经过节点的路径(从当前节点到任意叶子节点的路径唯一)
想象从任意节点出发,沿着所有可能的路径走向叶子节点(NIL节点),每条路径经过的黑色节点数量必须完全相同。这个统一的计数被称为该节点的黑高(Black Height)。
以节点X为起点,可能存在多条通往不同NIL叶子的路径
路径可能经过不同数量的红色节点
但所有路径中的黑色节点总数必须严格一致B(黑)/ \R(红) B(黑)/ \ / \ NIL NIL NIL NIL
如上代码,根节点到最左NIL:B→R→NIL(黑高=1)
根节点到最右NIL:B→B→NIL(黑高=2)→ 违反规则(实际红黑树不会出现此结构)
黑高的意义是什么??
我们前面已经了解了,不可能有连续的两个红节点,所以一定是一黑一红,或者两黑。那么通过黑高(Black-Height)的严格一致,将最长路径限制在最短路径的两倍以内,从而保证树高始终维持在O(log n)级别。
这里再给大家放一下红黑树的各种示意图,能对性质有更深的理解。
正是黑高的存在,所以满足上面的性质,红黑树就能保证:其最长路径中节点个数不会超过最短路径节点个数的两倍。
三、红黑树实现的总体思路
红黑树包含了之前的AVL树的四大旋转,它没有像AVL树那样严格的平衡,仅要求:最长路径不超过最短路径的两倍即可。但是插入节点是根据大小选择性的走左还是走右,我们是无法控制插入位置的,那么如何保证插入节点后依然属于红黑树?
这里我们提供三板斧:
1.标准BST插入
按照二叉搜索树规则找到插入位置,新节点初始设为红色(最小化对黑高的影响)
设为黑色的代价:必然导致某条路径黑高+1
需要递归调整整棵树的所有路径 平均需O(log n)次调整
设为红色的代价:仅可能产生双红冲突(父节点也是红色)
通过有限的旋转和变色即可修复 平均只需O(1)次调整
2.冲突检测
检测与父节点的冲突。
3.检查修复红黑树
检查黑高的改变有没有影响uncle节点。如有需改变。
四、红黑树的节点结构
// 节点颜色:红或黑
enum Color { RED, BLACK };// 红黑树节点
template<class K, class V>
struct RBTreeNode {// 存储的键值对std::pair<K, V> kv;// 节点指针RBTreeNode* parent; // 父节点RBTreeNode* left; // 左孩子RBTreeNode* right; // 右孩子// 节点颜色Color color; // 颜色标记// 构造函数RBTreeNode(const std::pair<K, V>& kv): kv(kv), // 初始化键值对parent(nullptr),// 父节点初始为空left(nullptr), // 左孩子初始为空right(nullptr), // 右孩子初始为空color(RED) // 新节点默认红色(符合红黑树规则){}
};
int main() {// 创建一个红黑树节点RBTreeNode<int, std::string> node({10, "Hello"});// 访问节点成员std::cout << node.kv.first; // 输出键:10std::cout << node.kv.second; // 输出值:"Hello"// 检查节点颜色if(node.color == RED) {std::cout << "这是红色节点";}return 0;
}
五、红黑树的插入操作
首先我们需要先找到那个插入位置:如果是根(颜色为黑);如果是其它节点(颜色为红)
1.按照二叉搜索的树规则插入新节点
// 插入新节点
bool insert(int key, string value) {// 1. 如果树是空的,直接创建根节点if (root == nullptr) {root = new Node(key, value);root->color = BLACK; // 根节点必须是黑色return true;}// 2. 寻找插入位置Node* parent = nullptr;Node* current = root;while (current != nullptr) {parent = current;if (key < current->key) { // 向左找current = current->left;} else if (key > current->key) { // 向右找current = current->right;}else { // 已经存在相同的keyreturn false; }}// 3. 创建新节点(默认为红色)Node* newNode = new Node(key, value);newNode->parent = parent; // 连接父节点// 4. 连接到父节点if (key < parent->key) {parent->left = newNode; // 作为左孩子} else {parent->right = newNode; // 作为右孩子}// 5. 这里应该添加红黑树的平衡调整代码// fixInsert(newNode);return true;
}
2.检测新节点插入后,红黑树的性质是否造到破坏
因为新节点的默认颜色是红色,因此:如果其双亲节点的颜色是黑色,没有违反红黑树任何
性质,则不需要调整;但当新插入节点的双亲节点颜色为红色时,就违反了性质三不能有连
在一起的红色节点,此时需要对红黑树分情况来讨论:
约定:cur为当前节点,p为父节点,g为祖父节点,u为叔叔节点
情况一: cur为红,p为红,g为黑,u存在且为红
总结,解决方式:将p,u改为黑,g改为红,然后把g当成cur,继续向上调整。
情况二: cur为红,p为红,g为黑,u不存在/u存在且为黑
这里我们会发现仅靠变色无法修复,因为会导致黑高不一致。必须通过 旋转 调整树结构,再配合变色。也就是右旋再去判断。
- p为g的左孩子,cur为p的左孩子,则进行右单旋转;
- 相反, p为g的右孩子,cur为p的右孩子,则进行左单旋转
- p、g变色--p变黑,g变红
拓展说明:u的情况有两种
1.如果u节点不存在,则cur一定是新插入节点,因为如果cur不是新插入节点,
则cur和p一定有一个节点的颜色是黑色,就不满足性质4:每条路径黑色节点个
数相同。
2.如果u节点存在,则其一定是黑色的,那么cur节点原来的颜色一定是黑色的,
现在看到其是红色的原因是因为cur的子树在调整的过程中将cur节点的颜色由
黑色改成红色。
🔍 为什么u不存在时cur一定是新插入节点?
黑(g)/ \红(p) NIL(黑)/ 红(cur) ← 新插入节点
如果cur不是新插入的节点,而u是NIL(黑色),那么从祖父节点g出发:
路径1:g → p → cur → NIL
路径2:g → u(NIL)
路径1的黑高:g(1) → p(不算) → cur(不算) → NIL(1) → 总计:2
路径2的黑高:g(1) → NIL(1) → 总计:2
如果cur原本就存在且为黑色:
黑(g)/ \红(p) NIL(黑)/ 黑(cur) ← 违反性质4!
路径1黑高:g(1) → p(不算) → cur(1) → NIL(1) → 总计:3
路径2黑高:g(1) → NIL(1) → 总计:2所以不可能,结论:只有当cur是新插入节点时,才可能遇到u不存在的情况
🔍 为什么cur"原来的颜色"是黑色?
这里的"原来"指的是在插入新节点前的状态。考虑红黑树的性质:每次插入新节点时都设为红色(初始红色)
如果cur不是新插入节点,那么它之前一定是黑色节点被改为红色,这就说明,u存在的时候,一定是自下而上调整成为黑色的。
情况三: cur为红,p为红,g为黑,u不存在/u存在且为黑
p为g的左孩子,cur为p的右孩子,则针对p做左单旋转;相反, p为g的右孩子,cur为p的左孩子,则针对p做右单旋转即可
六、红黑树的验证
我们来根据下面几个性质检测红黑树:
- 根节点是否为黑色
- 是否有连续的红色节点
- 根据任意一条路径的黑色节点去测一棵树的黑高,看是否相等
总代码
#include <iostream>
#include <utility>
using namespace std;// 节点颜色:红或黑
enum Color { RED, BLACK };// 红黑树节点
template<class K, class V>
struct RBTreeNode {// 存储的键值对pair<K, V> kv;// 节点指针RBTreeNode* parent; // 父节点RBTreeNode* left; // 左孩子RBTreeNode* right; // 右孩子// 节点颜色Color color; // 颜色标记// 构造函数RBTreeNode(const pair<K, V>& kv): kv(kv), // 初始化键值对parent(nullptr), // 父节点初始为空left(nullptr), // 左孩子初始为空right(nullptr), // 右孩子初始为空color(RED) // 新节点默认红色(符合红黑树规则){}
};// 红黑树类
template<class K, class V>
class RBTree {
public:typedef RBTreeNode<K, V> Node;Node* root = nullptr;// 插入键值对bool insert(const K& key, const V& value) {// 1. 如果树是空的,直接创建根节点if (root == nullptr) {root = new Node(make_pair(key, value));root->color = BLACK; // 根节点必须是黑色return true;}// 2. 寻找插入位置Node* parent = nullptr;Node* current = root;while (current != nullptr) {parent = current;if (key < current->kv.first) { // 向左找current = current->left;}else if (key > current->kv.first) { // 向右找current = current->right;}else { // 已经存在相同的keyreturn false;}}// 3. 创建新节点(默认为红色)Node* newNode = new Node(make_pair(key, value));newNode->parent = parent; // 连接父节点// 4. 连接到父节点if (key < parent->kv.first) {parent->left = newNode; // 作为左孩子}else {parent->right = newNode; // 作为右孩子}// 5. 平衡调整fixInsert(newNode);return true;}// 左旋void rotateLeft(Node* x) {Node* y = x->right;x->right = y->left;if (y->left != nullptr) {y->left->parent = x;}y->parent = x->parent;if (x->parent == nullptr) {root = y;} else if (x == x->parent->left) {x->parent->left = y;} else {x->parent->right = y;}y->left = x;x->parent = y;}// 右旋void rotateRight(Node* y) {Node* x = y->left;y->left = x->right;if (x->right != nullptr) {x->right->parent = y;}x->parent = y->parent;if (y->parent == nullptr) {root = x;} else if (y == y->parent->left) {y->parent->left = x;} else {y->parent->right = x;}x->right = y;y->parent = x;}// 插入后修复红黑树性质void fixInsert(Node* z) {while (z != root && z->parent->color == RED) {if (z->parent == z->parent->parent->left) {Node* y = z->parent->parent->right;if (y != nullptr && y->color == RED) {// 情况1:叔叔是红色z->parent->color = BLACK;y->color = BLACK;z->parent->parent->color = RED;z = z->parent->parent;} else {if (z == z->parent->right) {// 情况2:叔叔是黑色,z是右孩子z = z->parent;rotateLeft(z);}// 情况3:叔叔是黑色,z是左孩子z->parent->color = BLACK;z->parent->parent->color = RED;rotateRight(z->parent->parent);}} else {Node* y = z->parent->parent->left;if (y != nullptr && y->color == RED) {// 情况1镜像z->parent->color = BLACK;y->color = BLACK;z->parent->parent->color = RED;z = z->parent->parent;} else {if (z == z->parent->left) {// 情况2镜像z = z->parent;rotateRight(z);}// 情况3镜像z->parent->color = BLACK;z->parent->parent->color = RED;rotateLeft(z->parent->parent);}}}root->color = BLACK;}// 中序遍历打印树void inorderTraversal(Node* node) {if (node == nullptr) return;inorderTraversal(node->left);cout << node->kv.first << "(" << (node->color == RED ? "R" : "B") << ") ";inorderTraversal(node->right);}// 打印树结构void printTree() {inorderTraversal(root);cout << endl;}
};int main() {RBTree<int, string> tree;// 测试插入操作tree.insert(10, "Apple");tree.insert(20, "Banana");tree.insert(5, "Cherry");tree.insert(15, "Date");tree.insert(25, "Elderberry");// 打印树结构cout << "红黑树中序遍历结果: ";tree.printTree();// 测试节点创建RBTreeNode<int, string> node({30, "Fig"});cout << "\n独立节点测试:" << endl;cout << "键: " << node.kv.first << endl;cout << "值: " << node.kv.second << endl;cout << "颜色: " << (node.color == RED ? "红色" : "黑色") << endl;return 0;
}