💁♂️个人主页:进击的荆棘
👇作者其它专栏:
目录
1.红黑树的概念
2.红黑树的实现
1.红黑树的概念
红黑树是一颗二叉搜索树,它的每一个节点增加一个存储位来表示节点的颜色,可以是红色或黑色。通过对任意一条从根到叶子的路径上各个节点的颜色进行约束,红黑树确保没有一条路径会比其他路径长出2倍,因而是接近平衡的。
1.1红黑树的规则:
1.每个节点不是红色就是黑色
2.根节点是黑色的
3.若一个节点是红色的,则它的两个孩子节点必须是黑色的,也就是说任意一条路径不会有连续的红色节点。
4.对于任意一个节点,从该节点到其所有NULL节点的简单路径上,均包含相同数量的黑色节点
说明:《算法导论》等书籍上补充了一条每个叶子节点(NIL》都是黑色的规则。这里所指的叶子节点不是传统意义上的叶子节点,而是我们所说的空节点,有些书籍上也把NIL叫做外部节点。NIL是为了方便准确标识出所有路径,《算法导论》在后续讲解实现的细节中也忽略了NIL节点,只是了解一下这个概念。





1.2红黑树是如何确保最长路径不超过最短路径的2倍的?
●由规则4可知,从根到NULL节点的每条路径都有相同数量的黑色节点,所以极端场景下,最短路径就是全是黑色节点的路径,假设最短路径长度为bh(black height)。
●由规则2和规则3可知,任意一条路径不会有连续的红色节点,所以极端场景下,最长的路径就是一黑一红间隔组成,那么最长路径的长度为2*bh。
●综合红黑树的4点规则而言,理论上的全黑最短路径和一黑一红的最长路径并不是在每棵红黑树都存在的。假设任意一条从根到NULL节点路径的长度为x,那么hb<=h<=2*hb。
1.3红黑树的效率
假设N是红黑树树中节点数量,h最短路径的长度,那么2^h-1<=N<2^(2*h)-1,由此退出h≈logN,也就是意味着红黑树增删查改最坏也就是走最长路径2*logN,那么时间复杂度还是O(logN)。
红黑树的表达相对AVL树要抽象一些,AVL树通过高度差直观的控制了平衡。红黑树通过4条规则的颜色约束,间接的实现了近似平衡,他们效率都是同一档次,但是相对而言,插入相同数量的节点,红黑树的旋转次数是更少的,因为它对平衡的控制没那么严格。


2.红黑树的实现
2.1红黑树的结构
cpp
enum Color{
RED,BLACK
};
template<class k,class v>
struct AVLTreeNode{
//需要parent指针
pair<k,v> _kv;
AVLTreeNode<k,v>* _left;
AVLTreeNode<k,v>* _right;
AVLTreeNode<k,v>* _parent;
//记录红黑
Color _color;
AVLTreeNode(const pair<k,v>& kv)
:_kv(kv)
,_left(nullptr)
,_right(nullptr)
,_parent(nullptr)
{}
};
template<class k,class v>
class AVLTree{
typedef AVLTreeNode<k,v> Node;
public:
private:
Node* _root=nullptr;
};
2.2红黑树的插入
2.2.1红黑树插入一个值的大概过程
1.插入一个值按二叉搜索树规则进行插入,插入后我们只需要观察是否符合红黑树的4条规则。
2.若是空树插入,新增节点是黑色节点。若是非空树插入,新增节点必须是红色节点,因为非空树插入,新增黑色节点就破坏了规则4,规则4很难维护。
3.非空树插入后,新增节点必须是红色节点,若父节点是黑色的,则没有违反任何规则,插入结束。
4.非空树插入后,新增节点必须是红色节点,若父节点是红色的,则违反规则3。进一步分析,c是红色,p是红色,则g一定为黑色,这3个颜色都固定了,关键的变化看u的情况,需要根据u分为以下几种情况分别处理。
说明:下图中假设我们把新增节点标识为c(cur),c的父亲节点标识为p(parent),p的父亲节点标识为g(grandfather),p的兄弟节点标识为u(uncle)。
2.2.2情况1:变色
c为红,p为红,g为黑,u存在且为红,则将p和u变黑,g变红。再把g当作新的c,继续往上更新。
分析:因为p和u都是红色,g是黑色,把p和u变黑,左边子树路径个增加一个黑色节点,g再变红,相当于保持g所在子树的黑色节点的数量不变,同时解决了c和p连续红色节点的问题,需要继续往上更新是因为,g是红色,若g的父亲还是红色,那么就还需要继续处理;若g的父亲是黑色,则处理结束了;若g就是整棵树的根,再把g变回黑色。
情况1只变色,不旋转。所以无论c是p的左还是右,p是g的左还是右,都是上面的变色处理方式。

●跟AVL树类似,图0给我们展示了一种具体情况,但是实际中需要这样处理的有很多种情况。
●图1将以上类似的处理进行了抽象表达,d/e/f代表每条路径拥有bh个黑色节点的子树,a/b代表每条路径拥有bh-1个黑色节点的根为红的子树,hb>=0。
●图2/图3/图4,分别展示了hb==0/hb==1/hb==2的具体情况组合分析,当bh等于2时,这里组合情况上百亿种,这些样例是帮助我们理解的,不论情况多少种,多么复杂,处理方式都一样,变色再继续往上处理即可,所以我们只需看抽象图即可。




2.2.3情况2:单旋+变色
c为红,p为红,g为黑,u不存在或u存在且为黑,u不存在,则c一定是新增节点,u存在且为黑,则c一定不是新增,c之前是黑色的,是在c的子树中插入,符合情况1,变色将c从黑色变成红色,更新上来的。
分析:p必须变黑,才能解决连续红色节点的问题,u不存在或是黑色的,这里单纯的变色无法解决问题,需要旋转+变色。
g
p u
c
若p是g的左,c是p的左,那么以g为旋转点进行右单旋,再把p变黑,g变红即可。p变成这棵子树的新的根,这样子树黑色节点的数量不变,没有连续的红色节点了,且不需要往上更新,因为p的父亲是黑色还是红色或者为空都不违反规则。
g
u p
c
若p是g的右,c是p的右,那么以g为旋转点进行左单旋,再把p变黑,g变红即可。p变成这颗子树的新的根,这样子树黑色节点的数量不变,没有连续的红色节点了,且不需要往上更新,因为p的父亲是黑色还是红色或者为空都不违反规则。


2.3.4情况3:双旋+变色
c为红,p为红,g为黑,u不存在或u存在且为黑,u不存在,则c一定是新增节点,u存在且为黑,则c一定不是新增,c之前是黑色的,是在c的子树中插入,符合情况1,变色将c从黑色变成红色,更新上来的。
分析:p必须变黑,才能解决,连续红色节点的问题,u不存在或是黑色的,单纯的变色无法解决问题,需要旋转+变色。
g
p u
c
若p是g的左,c是p的右,那么先以p为旋转点进行左单旋,再以g为旋转点进行右单旋,再把c变黑,g变红即可。c变成这颗子树新的根,这样子树黑色节点的数量不变,没有连续的红色节点了,且不需要往上更新,因为c的父亲是黑色还是红色或者为空都不违反规则。
g
u p
c
若p是g的右,c是p的左,那么先以p为旋转点进行右单旋,再以g为旋转点进行左单旋,再把c变黑,g变红即可。c变成这颗子树新的根,这样子树黑色节点的数量不变,没有连续的红色节点了,且不需要往上更新,因为c的父亲是黑色还是红色或者为空都不违反规则。


2.3红黑树的插入代码实现
cpp
//红黑树的插入代码和AVL树的插入代码很相似,只不过没有平衡因子
bool Insert(const pair<k,v>& kv){
if(!_root){
_root=new Node(kv);
//根节点必须为黑色
_root->_color=BLACK;
return true;
}
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->_color=RED;
if(cur->_kv.first<parent->_kv.first)
parent->_left=cur;
else parent->_right=cur;
//父指针指向父节点
cur->_parent=parent;
//当父节点存在,且与新插入节点构成连续红色时
while(parent&&parent->_color==RED){
Node* grandfather=parent->_parent;
//若父节点在爷节点的左边
if(parent==grandfather->_left){
// g
//p u
Node* uncle=grandfather->_right;
//若u存在且为红色,变色
if(uncle&&uncle->_color==RED){
parent->_color=uncle->_color=BLACK;
grandfather->_color=RED;
//将c更新到g,继续操作
cur=grandfather;
parent=cur->_parent;
}
//此时u不存在或为黑色
else{
//当插入的cur在parent的左边
if(cur==parent->_left){
// g
// p u
//c
RotateR(grandfather);
parent->_color=BLACK;
grandfather->_color=RED;
}
//当插入的cur在parent的右边
else {
// g
// p u
// c
RotateL(parent);
RotateR(grandfather);
parent->_color=BLACK;
grandfather->_color=RED;
}
break;
}
}
//当父节点在爷爷的右边
else{
// g
//u p
Node* uncle=grandfather->_left;
//若u存在且为红色,变色
if(uncle&&uncle->_color==RED){
parent->_color=uncle->_color=BLACK;
grandfather->_color=RED;
//将c更新到g,继续操作
cur=grandfather;
parent=cur->_parent;
}
//此时u不存在或为黑色
else{
//当插入的cur在parent的右边
if(cur==parent->_right){
// g
// u p
// c
RotateL(grandfather);
parent->_color=BLACK;
grandfather->_color=RED;
}
//当插入的cur在parent的左边
else {
// g
// u p
// c
RotateR(parent);
RotateL(grandfather);
parent->_color=BLACK;
grandfather->_color=RED;
}
break;
}
}
}
//根节点必须为黑色
_root->_color=BLACK;
return true;
}
2.4红黑树的查找
按二叉搜索树逻辑实现即可,搜索效率为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;
}
2.5红黑树的验证
这里获取最长路径和最短路径,检查最长路径不超过最短路径的2倍是不可行的,因为就算满足这个条件,红黑树也可能颜色不满足规则,当前展示没出问题,后续继续插入还是会出问题的。所以我们还是去检查4点规则,满足这4点规则,一定能保证最长路径不超过最短路径的2倍。
1.规则1枚举颜色类型,天然实现保证了颜色不是黑色就是红色。
2.规则2直接检查根即可。
3.规则3前序遍历检查,遇到红色节点查孩子不太方便,因为孩子有两个,且不一定存在,反过来检查父亲的颜色就方便多了。
4.规则4前序遍历,遍历过程中用形参记录根到当前节点的blaNum(黑色节点数量),前序遍历遇到黑色节点就blaNum++,走到空就计算出了一条路径的黑色节点数量。再任意以一条路径黑色节点数量作为参考值,依次比较即可。
