前面一节我们介绍了平衡搜索二叉树AVL树,我们知道,AVL树虽然查找效率很高,但是不能过多的修改,因为它为了保持平衡要不断的进行旋转。我们今天介绍的红黑树也是一种平衡搜索树,不过它所要求的平衡没有AVL树那么严格,因此对它进行修改操作时所要进行的旋转比AVL树要进行的旋转少。
红黑树,一种二叉搜索树,它额外在每一个结点上增加一个表示结点颜色的存储位,有Red和Black两种可能。
通过对任意一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长处二倍,因此它是接近平衡的。
//结点的颜色
enum color
{
RED,
BLACK
};
//结点的定义
template<typename T>
struct RBnode//红黑树的结点(三叉链)
{
RBnode(T data)
:_data(data)
, _parent(nullptr)
, _left(nullptr)
, _right(nullptr)
, col(RED)
{}
T _data;//数据
RBnode* _parent;//父节点(为了方便实现旋转)
RBnode* _left;//左孩子
RBnode* _right;//右孩子
color _col;//颜色
};
看到这里,相信大家会有一个疑问:为什么默认新插入的结点颜色是红色?
答:如果我们将新结点的颜色设置为黑色,那么它一定会违背性质4(即,对于每一个结点,从该节点到其所有后代叶子结点的简单路径上,均包含相同数目的黑色结点),这样我们就需要大幅度的在这棵树上进行调整(几乎需要所有路径进行调整),才能使它再次符合性质4;
如果我们将新结点颜色设置为红色,它有可能违背性质3(即,如果一个结点是红色,那么它的两个孩子结点是黑色),也有一定的可能不违背,即使违背性质3它所带来的后果比违背性质4严重性小很多,我们可以通过几次旋转(只对它所在的路径进行调整)使它重新符合性质3。
因为我们新插入一个结点,它不是黑色就是红色,因此一定会违反一条性质,我们选择违反后果较小的性质3。
bool insert(const pair<K, V>& kv)
{
Node* newnode = new RBnode<K,V>(kv);
if (!_root)//如果根节点为空,则新插入的结直接就是根节点
{
_root = newnode;
}
else
{
Node* cur = _root;
Node* parent = cur;
while (cur)
{
parent = cur;
if (cur->_kv.first > kv.first)
{
cur = cur->_left;
}
else if (cur->_kv.first < kv.first)
{
cur = cur->_right;
}
else//树中已经有这个结点,插入失败
{
return false;
}
}
cur = newnode;
if (parent->_kv.first > cur->_kv.first)
{
parent->_left = cur;
}
else
{
parent->_right = cur;
}
cur->_parent = parent;
Node* Grandpa = parent->_parent;
Node* uncle = nullptr;
while(Grandpa && parent->_col == RED)//如果父节点不是黑色,且父节点不是根结点
{
if (parent == Grandpa->_left)//定义叔叔结点
{
uncle = Grandpa->_right;
}
else
{
uncle = Grandpa->_left;
}
if (uncle->_col == RED)//如果叔叔存在且为红
{
Grandpa->_col = RED;
parent->_col = uncle->_col = BLACK;
}
else//如果叔叔不存在,或者存在且为黑
{
//p是g的右孩子,c是p的右孩子(左单旋)
if (parent == Grandpa->_right && cur == parent->_right)
{
Rotetal(Grandpa);
Grandpa->_col = RED;
parent->_col = BLACK;
}
//p是g的左孩子,c是p的左孩子(右单旋)
else if (parent == Grandpa->_left && cur == parent->_left)
{
Rotetal(Grandpa);
Grandpa->_col = RED;
parent->_col = BLACK;
}
//p是g的左孩子,c是p的右孩子(左右双旋)
else if (parent == Grandpa->_left && cur == parent->_right)
{
//先以parent为轴进行左单旋
Rotetal(parent);
//再以Grandpa为轴进行右单旋
Rotetar(Grandpa);
//更新颜色
cur->_col = BLACK;
Grandpa->_col = parent->_col = RED;
}
//p是g的右孩子,c是p的左孩子(右左双旋)
else if (parent == Grandpa->_right && cur == parent->_left)
{
//先以parent为轴进行右单旋
Rotetar(parent);
//再以Grandpa为轴进行左单旋
Rotetal(Grandpa);
//更新颜色
cur->_col = BLACK;
Grandpa->_col = parent->_col = RED;
}
//旋转之后就符合性质4了,因此不用再继续更新
break;
}
cur = parent;
parent = Grandpa;
Grandpa = Grandpa->_parent;
}
if (_root->_col == RED)//如果到最后更新到根节点,导致根节点为红色,为了满足性质2(根节点是黑色),就要将根结点置为黑色
{
_root->_col = BLACK;
}
}
return true;
}
//左单旋
void Rotetal(Node* parent)
{
Node* SubR = parent->_right;
Node* SubRL = SubR->_left;
Node* Grandpa = parent->_parent;
parent->_parent = SubR;
parent->_right = SubRL;
if (SubRL)
{
SubRL->_parent = parent;
}
SubR->_parent = Grandpa;
if (!Grandpa)
{
_root = SubR;
_root->_parent = nullptr;
}
else
{
if (parent == Grandpa->_left)
{
Grandpa->_left = SubR;
}
else
{
Grandpa->_right = SubR;
}
}
SubR->_left = parent;
}
//右单旋
void Rotetar(Node* parent)
{
Node* SubL = parent->_left;
Node* SubLR = SubL->_right;
Node* Grandpa = parent->_parent;
parent->_parent = SubL;
parent->_left = SubLR;
if (SubLR)
{
SubLR->_parent = parent;
}
SubL->_parent = Grandpa;
if (!Grandpa)
{
_root = SubL;
_root->_parent = nullptr;
}
else
{
if (parent == Grandpa->_left)
{
Grandpa->_left = SubL;
}
else
{
Grandpa->_right = SubL;
}
}
SubL->_right = parent;
}
如果该树是空树,就让新结点作为它的根节点。
先找到要插入的位置,比当前结点小就向左子树寻找,比当前结点大就向右子树寻找。
新结点默认为红色,
1.如果双亲节点的颜色是黑色,则没有违反红黑树性质,不需要调整;
2.如果双亲节点的颜色是红色,则违反性质4需要进行调整。
为了方便分析,我们约定当前结点为cur©,当前节点的父节点为parent§,当前节点的祖父结点为Grandpa(g),当前结点的叔叔结点为uncle(c).。
升序:
降序:
随机插入构建红黑树:
验证红黑树分为两步:
中序遍历是否为有序序列
代码如下:
bool IsValidRBTree()//验证是否为红黑树
{
Node* pRoot = GetRoot();
// 空树也是红黑树
if (nullptr == pRoot)
return true;
// 检测根节点是否满足情况
if (BLACK != pRoot->_col)
{
cout << "违反红黑树性质二:根节点必须为黑色" << endl;
return false;
}
// 获取任意一条路径中黑色节点的个数
size_t blackCount = 0;
Node* pCur = pRoot;
while (pCur)
{
if (BLACK == pCur->_col)
blackCount++;
pCur = pCur->_left;
}
// 检测是否满足红黑树的性质,k用来记录路径中黑色节点的个数
size_t k = 0;
return _IsValidRBTree(pRoot, k, blackCount);
}
bool _IsValidRBTree(Node* pRoot, size_t k, const size_t blackCount)
{
//走到null之后,判断k和black是否相等
if (nullptr == pRoot)
{
if (k != blackCount)
{
cout << "违反性质四:每条路径中黑色节点的个数必须相同" << endl;
return false;
}
return true;
}
// 统计黑色节点的个数
if (BLACK == pRoot->_col)
k++;
// 检测当前节点与其双亲是否都为红色
Node* pParent = pRoot->_parent;
if (pParent && RED == pParent->_col && RED == pRoot->_col)
{
cout << "违反性质三:没有连在一起的红色节点" << endl;
return false;
}
return _IsValidRBTree(pRoot->_left, k, blackCount) &&
_IsValidRBTree(pRoot->_right, k, blackCount);
}
红黑树和AVL树都是高效的平衡二叉树,增删查改的时间复杂度都是O( l o g 2 N log_2 N log2N),红黑树不追求绝对平衡,只要保证最长路径不超过最短路径的两倍。相对而言,插入和旋转的次数更少,在经常进行增删的结构中性能比AVL树更优,而且红黑树的实现比AVL树简单,因此更加常用。
以上就是今天要讲的内容,本文介绍了C++中红黑树的相关概念。本文作者目前也是正在学习C++相关的知识,如果文章中的内容有错误或者不严谨的部分,欢迎大家在评论区指出,也欢迎大家在评论区提问、交流。
最后,如果本篇文章对你有所启发的话,希望可以多多支持作者,谢谢大家!