前言
无论是BST还是AVL树,树的中序遍历结果都是有序的,这是二叉搜索树的根本性质。不过AVL树比BST性能更上一层,因为AVL树在保持BST性质的同时,也保证树的高度是平衡的(左右子树高度相差不大于1)。
有了BST与AVL树的基础,我们今天来搞红黑树RBTree!那么红黑树是什么?
红黑树
红黑树也是二叉搜索树,不同于AVL,红黑树在树的每个节点上增加了一个颜色标志,用于表示节点的颜色(RED
or BLACK
)。红黑树通过对任何一条从根到叶子的简单路径上各个节点的颜色进行约束,保证没有一条路径会比其他路径长出2倍,因此红黑树是接近平衡的。
红黑树与AVL树
但既然已经有了AVL这种更平衡的树:
AVL树保证任意一条路径最多比其他路径多出一个节点,而红黑树是任意一条路径长度不会比其他路径长2倍!所以AVL树更加严格,更加平衡!
为什么还搞红黑树?不急,且听我大致说说。
AVL树与红黑树有以下几点区别:
(1) AVL树更加平衡!因此可以提供更快的查询速度(因为树的高度会更低),所以适用于读取查找操作较较的场景,但因为追求平衡的极致,会导致插入删除操作引发树结构调整的频次较高,耗费资源。
(2) 红黑树对平衡退而求其次,使得插入删除引发树结构调整的频次较低,因此比较适用于插入删除操作较多的场景。
(3) 一般来说,AVL树比红黑树更加难以平衡!
所以,有红黑树这种玩意儿也就不足为奇喽。
红黑树的性质
一棵红黑树必然是满足以下性质的BST:
- 每个节点颜色非黑即红
- 根节点必然是黑色的
- 每个叶子节点(Nil)是黑色的
- 如果一个节点是红色的,那么它的孩子颜色必然为黑(即不存在两个红节点相连)
- 对任意节点来讲,从该节点到叶子节点所包含的黑节点的数目是相等的
红黑树的节点及红黑树
红黑树的节点含有5个属性:color
(颜色)、left
(左孩子)、right
(右孩子)、parent
(父节点)、val
(存储值)。
若一个节点没有孩子或者没有父节点,则该节点相应指针属性的值为Nil
。
哨兵Nil
Nil
是为了便于处理红黑树代码中边界条件而设置的一个哨兵 ,是一个特殊的树节点,该指针父/左/右孩子均为nullptr
(建议如此设置),颜色为BLACK
。
有了Nil
后红黑树所有节点中本应指向nullptr
的指针都会指向Nil
,也就是说,根节点的父节点是Nil
,叶子节点的孩子也是Nil
。因此,红黑树中的叶子为了与传统含义上的叶子保持一致,因此将Nil
视为叶子节点。Nil
也算在红黑树的性质5中!
红黑树的类只有两个属性:root
与Nil
,其他都是一些方法。
整个红黑树框架如下:
typedef enum { RED = 0, BLACK }COLOR;
template <class Ty>
class RBTree;//声明
template<class Ty>
class RBNode {
friend class RBTree<Ty>;
public:
RBNode() :val(Ty()), color(RED), left(nullptr), right(nullptr), parent(nullptr) {}
RBNode(Ty _V = Ty()) :val(_V), color(RED), left(nullptr), right(nullptr), parent(nullptr) {}
~RBNode() {
val = Ty();
color = RED;
left = right = parent = nullptr;
}
private:
Ty val;
COLOR color;
RBNode* left;
RBNode* right;
RBNode* parent;
};
class RBTree {
typedef RBNode<Ty> Node;
public:
RBTree() :root(Nil), Nil(_BuyNode(Ty())) {//无参构造
Nil->left = Nil->right = Nil->parent = nullptr;
Nil->color = BLACK;
}
RBTree(vector<Ty> v) :root(Nil), Nil(_BuyNode(Ty())) {//vector构造
Nil->left = Nil->right = Nil->parent = nullptr;
Nil->color = BLACK;
for (auto& it : v)
Insert(it);
}
public:
bool Insert(const Ty& data) { return Insert(root, data); }//插入接口
bool Remove(const Ty& key) { return Remove(root, key); }//删除接口
protected:
bool Insert(Node*& root,const Ty& data);//插入函数
bool Remove(Node*& root,const Ty& key) ;//删除函数
void RotateL(Node*& t, Node* x);//左旋函数
void RotateR(Node*& t, Node* x);//右旋函数
void Insert_Fixup(Node*& t, Node* x);//插入调整
void Remove_Fixup(Node*& t, Node* x);//删除调整
private:
Node* _BuyNode(const Ty& data) {//申请节点
Node* node = new Node(data);
node->left = node->right = node->parent = Nil;
return node;
}
private:
Node* Nil;
Node* root;
};
如下图所示,便是一个红黑树:
红黑树的旋转与着色
为了保持树结构的平衡,红黑树也像AVL树一样,需要旋转操作。不过,AVL树只需要旋转即可完成平衡,而红黑树还需要额外的操作——着色。
我们先说旋转,再说着色。红黑树的旋转比起AVL树的旋转较为简单(不涉及复杂双旋),需要多次旋转的地方都是单旋的结合。
先声明一下两个旋转函数⑧:
左旋:RotateL(Node*& t,Node* x);
右旋:RotateR(Node*&t,Node* x);
两个函数第一个参数t
是红黑树的根节点,第二个参数x
是被旋转的结点,也就是位置深度较浅的节点。
因为节点有了parent
属性,这里为了显示具体逻辑,因此将parent
画出来,用绿线表示,left
与right
都用红线表示。
右旋
右旋前结构如上图中左边所示,右旋后如上图中右边所示。
逻辑与AVL的旋转极其相似,这里不多介绍,但是要指出一个注意事项,因为parent
的存在,所以旋转时,不能忘了改变parent
哦!另外,建议在旋转过程中,尽量不要修改Nil的任何属性,尤其是颜色!颜色是万万不能变的。
void RotateR(Node*& t, Node* x) {
Node* y = x->left;
x->left = y->right;
if (y->right != Nil)
y->right->parent = x;
if (x == t)
t = y;
else if (x == x->parent->left)
x->parent->left = y;
else
x->parent->right = y;
y->parent = x->parent;
y->right = x;
x->parent = y;
}
左旋
会了右旋,左旋也不是问题,基本上就是把右旋代码的left
替换为right
,right
替换为left
就可以了。
着色
着色顾名思义,给节点上色,改变节点的color
就可以,只需要一个赋值语句即可。
但这里要说明一下Nil为什么是黑色,以及每次创建新节点的默认颜色要设什么颜色。
回顾一下红黑树必须满足的性质:
- 每个节点颜色非黑即红
- 根节点必然是黑色的
- 每个叶子节点(Nil)是黑色的
- 如果一个节点是红色的,那么它的孩子颜色必然为黑(即不存在两个红节点相连)
- 对任意节点来讲,从该节点到叶子节点所包含的黑节点的数目是相等的
首先,因为Nil
被看做红黑树的真正叶子节点,因此Nil必须着为BLACK
。着为BLACK
也利于第一个数据的插入。为何这么说,诸君且先坐下。
现在新插入一个节点,假设节点的默认颜色是BLACK
,那么不管新插节点的父节点是什么颜色,必然要进行调整,因为插入黑节点,意味着插入的路径上比其他路径多了一个黑节点,违反了性质5,因此必须进行调整。
而如果插入红节点,其父节点是黑色,我们完全不用调整,因为没有违反任何一条性质,每条路径上的黑色节点数目还是一样的。只有当父节点是红色时,会违反性质4,引发调整。两相对比,新节点的默认颜色设为RED体验极佳!
再回归Nil
设为BLACK
为何也利于第一个节点的插入:当Nil
设为BLACK
,而第一个节点就是新建节点,默认为RED
,不违反性质4。接下来我们只需要将第一个节点再次着为BLACK
即可,这也并不违反任何性质!
试想一下,Nil
作为红色,首先违反性质3,且先按下不提。其次,插入第一个节点时,若Nil
颜色为RED
,两个RED
节点相连,违反性质4!因此Nil
设BLACK
极好!
红黑树的插入
想要插入新数据,必然要遵守BST的规则找到合适的插入位置,能找到就插入,找不到返回即可。
但有一个问题是,插入有可能引发调整,因此插入后需要进行检测,如果某一性质被破坏,需要进行调整Insert_Fixup
bool Insert(Node*& t, const Ty& data) {
//先按BST插入
Node* pr = Nil;//节点
Node* x = t;
while (x != Nil) {
if (data == x->val)//不允许插入已存在数据
return false;
pr = x;//父节点跟进
if (data > x->val)//进右树
x = x->right;
else//左树
x = x->left;
}
x = _BuyNode(data);//新建节点
if (pr == Nil)//插入目标是第一个节点
t = x;
else if (data < pr->val)//成为左树
pr->left = x;
else//成为右树
pr->right = x;
x->parent = pr;//新建节点父指针指向父节点
//调整平衡
Insert_Fixup(t, x);
return true;
}
那么调整部分Insert_Fixup
怎么说?哪种情况会引发调整?那么我们首先需要分析插入接点会违反红黑树的哪些性质。
- 每个节点颜色非黑即红
- 根节点必然是黑色的
- 每个叶子节点(Nil)是黑色的
- 如果一个节点是红色的,那么它的孩子颜色必然为黑(即不存在两个红节点相连)
- 对任意节点来讲,从该节点到叶子节点所包含的黑节点的数目是相等的
首先,因为新插节点x
的默认颜色为RED
,那么性质1,3,5必然满足。被破坏的只能是性质2和性质4。如果插入节点x
是根节点,那么性质2就会被破坏;如果插入节点x
的父节点x->parent
是红色,性质4就会被破坏。因此我们每插入一个新节点,判断是否需要调整的条件就是x->parent
是否是红色!。又因为调整一般来说像AVL一样,需要向上回溯调整,所以Insert_Fixup
里面必然是循环!
接下来我们分析需要调整的几种情况,如下(x->color:RED
):
情况1:叔节点为红
这种情况下,我们只需要将x
的父节点A和叔节点D染为BLACK
,再将x
的祖父节点C染为RED
。如此染色后,当前子树已满足红黑树的性质,但由于我们将C染为RED
,可能导致更上层的树结构的性质被破坏,所以需要向上回溯,令C成为新的x
,继续向上回溯调整。
总结一下就是:
1.
x->parent->color = uncle->color = BLACK
2.x->parent->parent->color=RED
3.x=x->parent->parent
(uncle
表示x
的叔节点)
如下所示:
情况2:叔节点为黑且x为左孩子
情况2下,我们只需将x
的父节点A染黑,x
的祖父节点C染红,然后对祖父节点C进行一次右旋RotateR
即可,该情况下x
也无需向上回溯,因此根节点调整前后都为BLACK
,不会影响上层的性质,所以调整到此也就宣告结束了。
总结一下:
1.
x->parent->color=BLACK
,如图b
2.RotateR(root,x->parent->parent)
,如图c
情况3:叔节点为黑且x为右孩子
首先,令x
的父节点A成为新的x
,接着对x进行一次左旋Rotate(root,x)
,情况3就转化成了情况2!
总结一下就是:
1.
x=x->parent
,如图b;
2.对x
执行一次左旋RotateL(root,x)
,如图c;
3.此时情况3已经转化为了情况2,我们执行情况2的处理流程即可
Insert_Fixup
由于上述三种情况在左右子树中均存在,而叔节点的获取会因左右树发生调整,所以实际出现的情况有6种。插入的调整函数代码如下:
符号 | 说明 |
---|---|
Nil | 红黑树的哨兵节点 |
void Insert_Fixup(Node*& root,Node* x){
while (x->parent->color == RED) {//存在不平衡
Node* uncle = Nil;
if (x->parent == x->parent->parent->left) {//左分支
uncle = x->parent->parent->right;//叔叔
if (uncle->color == RED) {//情况1
x->parent->color = BLACK;
uncle->color = BLACK;
uncle->parent->color = RED;//爷爷为黑
x = uncle->parent;//向上回溯
}
else {
if (x == x->parent->right) {//情况3
x = x->parent;
RotateL(t, x);
}
x->parent->color = BLACK;//情况2
x->parent->parent->color = RED;
RotateR(t, x->parent->parent);
}
}
else {//右树
uncle = x->parent->parent->left;//叔叔
if (uncle->color == RED) {//情况1
x->parent->color = BLACK;
uncle->color = BLACK;
uncle->parent->color = RED;//爷爷为黑
x = uncle->parent;//向上回溯
}
else {
if (x == x->parent->left) {//情况3
x = x->parent;
RotateR(t, x);
}
x->parent->color = BLACK;//情况2
x->parent->parent->color = RED;
RotateL(t, x->parent->parent);
}
}
}
t->color = BLACK;//保证根节点为黑
}
}
红黑树的删除
红黑树的删除略微麻烦一些(真麻烦),不过仔细分析一下还是能搞的。
和AVL的删除一样,叶子节点直接删除,非叶子节点则转化为删除带一个孩子的节点或者叶子节点。
bool Remove(Node*& t, const Ty& key) {
//先找节点
Node* p = t, * c;
while (p != Nil) {//寻找删除目标
if (p->val == key)//找到
break;
if (key > p->val)//去右树找
p = p->right;
else//去左树找
p = p->left;
}
if (p == Nil)//不存在
return false;
if (p->left != Nil && p->right != Nil) {//转化为删只有一个孩子的节点or叶子
Node* q = p->right;//寻找后继节点(也可以找前驱节点)
while (q->left != Nil)
q = q->left;
p->val = q->val;//覆盖
p = q;//转为删后继节点
}
//至此,p是删除目标,且不可能存在两个孩子
if (p->left != Nil)//有左孩子
c = p->left;
else//右孩子(也不一定有,没有就是Nil)
c = p->right;
c->parent = p->parent;//孩子链接祖父
//祖父链接c成为c的父节点
if (p->parent == Nil)//删的根节点
t = c;
else if (p == p->parent->left)//被删目标原来是左孩子
p->parent->left = c;
else//被删目标是右孩子
p->parent->right = c;
//调整平衡
if (p->color == BLACK)
Remove_Fixup(t, c);
delete p;//删除
return true;
}
在删除这块,我们不将Nil
看作任意节点的子节点,也就是说当某节点的子节点是Nil
时,即认为它没有子节点。但这不意味着Nil
不存在哦!这样做方便分析。
首先我们先来看看删除都有哪些情况:
节点有两种颜色:RED
、BLACK
节点的孩子数目:0、1、2
那么根据排列组合的公式就有2*3=6种情况,如下:
1.被删节点无子节点,且被删节点为红色
这种情况直接删除就可以了,不影响红黑树的性质。
2.被删节点无子节点,且被删节点为黑色
较为复杂,建议看完其他情况再看这个!强烈建议!!!
删黑色节点必然会破坏红黑树的性质5
- 对任意节点来讲,从该节点到叶子节点所包含的黑节点的数目是相等的
现在删除这个黑色节点后,性质5被违反,红黑树不再是红黑树,此时我们为了维持红黑树的第5条性质,强行给该黑节点的孩子再加上一重黑色,令它的孩子一个能顶两个黑色节点。图示如下:
当然了!实际上我们并不会真的给B的孩子(Nil
)加上两重黑色,额外加一重黑色是理论,是为了阐述原理方便引入的。
现在理论上删除B后,B的父节点的一个孩子成为这个有两重黑色的节点。如下例所示:
这样看来,删除15后,红黑树的性质没有被破坏!但!这是因为我们引入理论上的双重黑节点,实践中是没有这种双重黑节点,因为这种节点违反了性质1!所以我们需要在理论上将这个双重黑节点通过调整恢复为单重黑节点。
要移除这个额外的一重黑色谈何容易!但也不是不可能,我们只需要将被删节点的父节点par
和兄弟节点bro
拿来,就可以完成!用par
和bro
的社会主义铁拳即可把这个双黑孙子打回一重黑色!
现在启程,准备开打!被删节点的par
和bro
告诉我他们有4种方法可以将这个孙子打回原形!
case 1.兄弟为黑,且有一个方向一致的红色子节点
方向一致:
若bro
是par
的左孩子,那么bro
也有一个左孩子为RED
;
若bro
是par
的右孩子,那么bro
也有一个右孩子为RED
;
如下所示(灰色表示颜色非红即黑):
这种情况下,我们需要对par进行旋转,并让bro与par交换颜色,再让双重黑节点把一重黑色让给bro的红色子节点。即可完成双黑的恢复,相应的这部分也完成了调整,根节点颜色调整前后未发生变化,红黑树调整完成。
我们取左图为例来做一个图示,如下:
总结一下:
1.
RotateR(root,par)
.如图b
2.bro->color = par->color
;
3.par->color = bro->right->color = BLACK
如图c
若是第二种,右旋变左旋,right
变left
即可,不再赘述。
case 2.兄弟为黑,且有一个方向不一致的红色子节点
上一个case已经说了方向一致,方向不一致相必也不用啰嗦了,一样有两种,画一种即可。
这种情况下,我们需要对bro
进行旋转,并让bro与bro->left交换颜色,这个case就转化成了case1,接下来执行case1的调整即可。
总结一下:
1.
bro->color =bro->left->color
;
2.bro->left->color = BLACK
;
3.RotateR(root,bro)
.如图b
4.执行case1
若是第二种,右旋变左旋,right
变left
即可,不再赘述。
case 3.兄弟为黑,无红色子节点
先将bro
染红,然后将双重黑节点的一重黑交给par
,成为上图中的右图。
不过此处需要注意的是,当par
为红,染黑后,调整也就结束了
但当par
为黑时,要让par成为新的被调整目标,要向上回溯继续调整。因为之前为黑,调整后还是黑,红黑树的性质5依旧没有被满足。
1.bro->color = RED;
2.par->color = BLACK;
3.if(par之前为黑) 令调整目标为par,继续向上回溯。
case 4.兄弟为红
兄弟为RED
,那么父亲必然为BLACK
!因为性质4不允许红红相连哦!
- 如果一个节点是红色的,那么它的孩子颜色必然为黑(即不存在两个红节点相连)
依然指画一种,另一种是对称的。
这种需要让par
左旋,并使bro
与par
交换颜色。此时就如右图所示,双重黑节并没有被打回原形,但它有了新兄弟A,这样就转化成了case1,2,3其中的一种,皮卡丘去吧,去别的地方继续执行调整吧!
1.bro->color = RED;
2.par->color = BLACK;
3.RotateL(root,par);
4.去执行其他地方执行调整
3.被删节点有一个子节点,且被删节点为红色
这种情况在红黑树中不可能存在!如下所示(纯黑节点为Nil
):
要出现这种情况,红黑树里需要存在上图中的结构,但这可能吗?不可能,这个结构已经不平衡了!
5.对任意节点来讲,从该节点到叶子节点所包含的黑节点的数目是相等的
从这个红节点出发,向右到Nil有1个黑节点,向左到Nil有2个黑节点,不平衡!红黑树里不存在这样的结构。
4.被删节有一个子节点,且被删节点为黑色
这种情况下,我们让被删节点的值等于子节点的值,再删除子节点就可以了。
5&6.被删节点有两个子节点
在Remove
函数处我们已经将删有两个孩子的节点转化为删后继节点,图示如下:
灰色表示节点的颜色未知,蓝色虚线将B,C链接起来,表示D不一定是C的直接子节点,但一定属于C的左子树,且是左子树的最小值。
当case1出现时,我们将D节点的值赋给B节点,case1就转化成了删无孩子的节点D(红框)。当D是红色的,就转化为了情况1;当D是黑色的,就转化为了情况2
当case2出现时,我们将C节点的值赋给B节点,case1就转化成了删有一个孩子的节点C(红框)。当C是红色的,就转化为了情况1;当C是黑色的,就转化为了情况2(III
是Nil
)或者 情况4(III
为非Nil
)
总之,情况5&6可以转化为情况1、2、4。
总结
下图就是上述情形的转化关系,观察可发现:
……情形真tm多! 虽然转化到最后就剩三种,但其他类型你还是得处理,让它发生转化!
Remove_fix
与Insert_Fixup
同理,上述情况也分左右树:
void Remove_Fixup(Node*& t, Node* x) {
//请注意!这里传入的是被删节点的孩子or后继节点(必然没有孩子)!!!此处可能需要好好思考一番!!!
//情形1在Remove里已基本处理,进 Remove_Fixup会直接执行最后一行代码,然后结束调整
//情形4,5,6在Remove里均已被转化为情形2,因此Remove_Fixup真正要解决的就是情形2
//情形2有4种case,将在while里一一被解决
//x==t说明x为根节点
//当孩子为黑可能需要调整
Node* w;//x的兄弟节点
while (x != t && x->color == BLACK) {//x非根节点且x的父节点为黑
if (x == x->parent->left) {//左树
w = x->parent->right;
if (w->color == RED) {//兄弟为红,父亲必黑 //case4
w->color = BLACK;
x->parent->color = RED;
w = w->left;//x的兄弟变了
RotateL(t, x->parent);
}
//兄弟为黑
if (w->left->color == BLACK && w->right->color == BLACK) { //case3
//兄弟无红子女
w->color = RED;
x = x->parent;
}
else {
if (w->left->color == RED) {//兄弟左孩子为红,右孩子为黑 //case2
w->color = RED;
w->left->color = BLACK;
w = w->left;
RotateR(t, w->parent);
}
w->color = x->parent->color;//case1
x->parent->color = BLACK;
w->right->color = BLACK;
RotateL(t, x->parent);
x = t;//设置即可结束循环
}
}
else {//右树
w = x->parent->left;
if (w->color == RED) {//case4
w->color = BLACK;
x->parent->color = RED;
w = w->right;//x的兄弟变了
RotateR(t, x->parent);
}
//兄弟为黑
if (w->left->color == BLACK && w->right->color == BLACK) { //case3
//兄弟无红子女
w->color = RED;
x = x->parent;
}
else {
if (w->right->color == RED) {//兄弟右孩子为红,左孩子为黑 //case2
w->color = RED;
w->right->color = BLACK;
w = w->right;
RotateL(t, w->parent);
}
w->color = x->parent->color;//case1
x->parent->color = BLACK;
w->left->color = BLACK;
RotateR(t, x->parent);
x = t;//设置即可结束循环
}
}
}
x->color = BLACK;//将根染黑(情形1)
}