
C++专栏:C++_Yupureki的博客-CSDN博客
目录
[1. 红黑树的概念](#1. 红黑树的概念)
[1.1 红黑树的规则](#1.1 红黑树的规则)
[1.2 红黑树如何保证平衡?](#1.2 红黑树如何保证平衡?)
[2. 红黑树的实现](#2. 红黑树的实现)
[3.1 基本结构](#3.1 基本结构)
[3.2 插入操作详解](#3.2 插入操作详解)
[3.2.1 变色](#3.2.1 变色)
[3.3 查找操作](#3.3 查找操作)
[3.4 红黑树的验证](#3.4 红黑树的验证)
上一篇:从零开始的C++学习生活 12:AVL树全面解析-CSDN博客
前言
前面我们学习了AVL树,一种高级的二叉搜索树,通过平衡因子控制树的高低差不超过2来保持树的平衡,可以大幅增加查找数据的效率
而今天,我们又有一种由AVL树而来的变种:红黑树。同样是高级的二叉搜索树,红黑树同样限制了树的最大高度来增加效率,但是方法有所差异
我将深入探讨红黑树的原理、特性及实现细节,帮助你全面理解这一重要数据结构的工作原理。

1. 红黑树的概念
红黑树是一种特殊的二叉搜索树,它在每个节点上增加了一个存储位来表示节点的颜色(红色或黑色)。通过对从根到叶子的任何路径上的节点颜色施加约束,红黑树确保没有任何一条路径会比其他路径长出两倍,因而保持了近似平衡的状态。

1.1 红黑树的规则
-
颜色规则:每个节点不是红色就是黑色
-
根节点规则:根节点必须是黑色的
-
红色节点规则 :红色节点的两个子节点必须是黑色的(即不能有连续的红色节点)
-
黑色高度规则 :从任意节点到其所有NULL 节点的简单路径上,包含相同数量的黑色节点
总地来说,红黑树顾名思义,只有红和黑,没有蓝白树或者其他的。
当然这里红和黑只是一种标志,你要想变成其他的颜色其实也可以awa
回答我们所说的,通过上面的规则分析,可以知道不能由连续的红色节点,即父亲和孩子不能同时为红色,但兄弟就不用,毕竟都不是连续的。但是可以存在连续的黑色节点
要注意的是,所有路径上的黑色节点数量相同,这里的路径指的是从根节点到任意一个NULL节点,不是直观的只算有节点的路径。
这样能够保证红黑树的最小长度是全黑(N),最大长度是红和黑交叉出现(2*N),因此限制了红黑树的高度
1.2 红黑树如何保证平衡?
红黑树通过上述四条规则巧妙地维持了树的平衡:
-
根据规则4,从根到NULL节点的每条路径都有相同数量的黑色节点。设最短路径(全黑路径)的黑色节点数为bh
-
根据规则2和3,最长路径由黑红节点交替组成,其长度不超过2×bh
-
因此,最长路径不会超过最短路径的两倍
假设红黑树有N个节点,最短路径长度为h,则有:
2^h - 1 < N < 2^(2×h) - 1
由此可得 h ≈ logN,这意味着红黑树的插入、删除和查找操作的最坏情况时间复杂度都是O(logN)。
与AVL树相比,红黑树对平衡的控制相对宽松,这使得在插入相同数量节点时,红黑树所需的旋转操作更少,整体性能更加稳定。
2. 红黑树的实现
3.1 基本结构
红黑树有红色和颜色,因此我们利用枚举常量
cpp
// 枚举值表示颜色
enum Colour {
RED,
BLACK
};
红黑树的节点和AVL树的节点相似,只不过多了颜色
cpp
// 红黑树节点
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)
{}
};
红黑树本体也和AVL树极其相似
cpp
template<class K, class V>
class RBTree {
typedef RBTreeNode<K, V> Node;
private:
Node* _root = nullptr;
// 其他成员函数...
};
3.2 插入操作详解
红黑树的插入我们默认按照二叉搜索树的规则
如果是非空树 ,插入的一定是红色节点,因为红黑树保证每条路径上的黑色节点相同。如果插入黑色节点,那么会破坏相等这一条件
如果是空树,插入的节点作为根节点,颜色设为黑色
之后要检查并修复红黑树性质:如果父节点是黑色,插入完成;如果父节点是红色,需要根据叔叔节点的颜色进行不同的处理
3.2.1 变色

假设我们插入的节点之前是红色节点,那么就会出现红红的情况,因此我们需要进行变色处理
这里我们把新插入的节点37作为孩子,40作为父亲,45作为祖父,48作为叔叔
情况1:叔叔节点存在且为红色 (变色)
在这里叔叔存在且为红色,那么我们就把parent和uncle变为黑色,grandparent变为红色
因为grandparent为红色之后,可能grandparent的parent也为红色,例如下图,因此我们还需向上检查,把child赋值为grandparent

情况2:叔叔节点不存在或为黑色(旋转+变色)
单旋情况:
对于上面的右图,叔叔虽然存在但是为黑色,那么我们还需要进行旋转处理
这里我们专门把child,parent,grandparent拆分出来(uncle无需关心,同时把偷偷把child变到左边去,child在右边是双旋情况)

我们对grandparent进行右单旋,同时把parent变为黑色,grandparent变为黑色
到了这种情况,uncle要么不存在要么为黑色,我们不用管uncle到底存不存在,也不用进行任何操作,继续保持grandparent指向uncle即可,别问,问就是巧妙awa,可以细品
我们再把child赋值为parent,由于parent已经为黑色了,不管parent和parent是红色还是黑色,都不会冲突,因此可以直接退出
双旋情况:
对于child在parent的右边情况,和AVL树类似,我们需要先对parent进行左单旋,保证三个节点在一条直线上,才能对grandparent进行右单旋

插入过程完成代码:
cpp
bool Insert(const K& key, const V& value)
{
if (_root == nullptr)
{
_root = new Node({key,value});
_root->_col = BLACK;
return true;
}
Node* newnode = new Node({ key,value });
newnode->_col = RED;
Node* cur = _root;
Node* child = cur;
while (cur)//按照二叉搜索树的规则插入
{
if (key < cur->_kv.first)
{
child = cur;
cur = cur->_left;
}
else if (key > cur->_kv.first)
{
child = cur;
cur = cur->_right;
}
else
{
find(key)->_kv.second++;
return true;
}
}
if (key < child->_kv.first)
{
child->_left = newnode;
newnode->_parent = child;
}
else
{
child->_right = newnode;
newnode->_parent = child;
}
child = newnode;
Node* parent = child->_parent;
while (child->_col == RED && parent && parent->_col == RED)
//当child为红色时并且parent为黑色时继续循环
{
parent = child->_parent;
Node* pparent = parent->_parent;
Node* uncle = nullptr;
if (pparent == nullptr)
break;
else
{
if (pparent->_left == parent)
uncle = pparent->_right;
else
uncle = pparent->_left;
if (uncle && uncle->_col == RED)//叔叔存在并且为红色直接变色
{
uncle->_col = BLACK;
parent->_col = BLACK;
pparent->_col = RED;
child = pparent;
parent = child->_parent;
}
else//叔叔不存在或者为黑色,需要旋转和变色
{
if (pparent->_left == parent)
{
if (parent->_right == child)
{
RotateL(parent);
RotateR(pparent);
child->_col = BLACK;
pparent->_col = RED;
}
else
{
RotateR(pparent);
parent->_col = BLACK;
pparent->_col = RED;
child = parent;
parent = child->_parent;
}
}
else
{
if (parent->_left == child)
{
RotateR(parent);
RotateL(pparent);
child->_col = BLACK;
pparent->_col = RED;
}
else
{
RotateL(pparent);
parent->_col = BLACK;
pparent->_col = RED;
child = parent;
parent = child->_parent;
}
}
}
}
}
_root->_col = BLACK;//在变色处理时可能会对根节点进行变动,根节点需要保持黑色
return true;
}
3.3 查找操作
红黑树的查找操作与普通二叉搜索树完全相同,时间复杂度为O(logN)。
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;
}
3.4 红黑树的验证
验证红黑树是否满足所有规则是确保实现正确的关键。验证方法包括:
-
检查根节点是否为黑色
-
检查是否存在连续的红色节点
-
检查所有路径的黑色节点数量是否相同
我们利用递归来实现
规则1很好判断
规则2我们对每个节点和其父节点检查即可
规则3我们可以定义一个专门统计一条路径上黑色节点数量的变量,一直传给下一个节点,随后判断左右两条路是否相等即可
cpp
bool Check(Node* root, int blackNum, const int refNum) {
if (root == nullptr) {
// 到达叶子节点,检查黑色节点数量
if (refNum != blackNum) {
cout << "存在黑色结点的数量不相等的路径" << endl;
return false;
}
return true;
}
// 检查是否存在连续的红色节点
if (root->_col == RED && root->_parent->_col == RED) {
cout << root->_kv.first << "存在连续的红色结点" << endl;
return false;
}
if (root->_col == BLACK) {
blackNum++;
}
return Check(root->_left, blackNum, refNum) &&
Check(root->_right, blackNum, refNum);
}