一.为什么要有红黑树这种数据结构?
首先,在二叉搜索树,极个别极端的数据会使二叉树退化成链表,所以出现了平衡二叉树(AVL树);
ALV树:是一种严格按照定义来实现的平衡二叉查找树,所以它查找的效率非常稳定,为O(log n),由于其严格按照左右子树高度差不大于1的规则,插入和删除操作中需要大量且复杂的操作来保持ALV树的平衡(左旋和右旋),因此ALV树适用于大量查询,少量插入和删除的场景中。
所以说,如果有大量数据进行插入,则AVL树会耗费大量时间去进行旋转平衡操作,为了使整个二叉树更具备查找与插入数据兼备的优点,就出现了红黑树。
二.红黑树
红黑树通过如下的性质定义实现自平衡:
1.节点是红色或黑色。
2.根是黑色。
3.所有叶子都是黑色(叶子是NIL节点)。
4.每个红色节点必须有两个黑色的子节点。(从每个叶子到根的所有路径上不能有两个连续的红色节点。)
5.从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点。
有了上面的几个性质作为限制,即可避免二叉查找树退化成单链表的情况。但是,仅仅避免这种情况还不够,这里还要考虑某个节点到其每个叶子节点路径长度的问题。如果某些路径长度过长,那么,在对这些路径上的及诶单进行增删查操作时,效率也会大大降低。这个时候性质4和性质5用途就凸显了,有了这两个性质作为约束,即可保证任意节点到其每个叶子节点路径最长不会超过最短路径的2倍。
当某条路径最短时,这条路径必然都是由黑色节点构成。当某条路径长度最长时,这条路径必然是由红色和黑色节点相间构成(性质4限定了不能出现两个连续的红色节点)。而性质5又限定了从任一节点到其每个叶子节点的所有路径必须包含相同数量的黑色节点。此时,在路径最长的情况下,路径上红色节点数量 = 黑色节点数量。该路径长度为两倍黑色节点数量,也就是最短路径长度的2倍。举例说明一下,请看下图:
红黑树创建代码:
#include<stdio.h>
#include<stdlib.h>
#define RED 0
#define BLACK 1
typedef int Elemtype;
typedef struct tree{
Elemtype data;
struct tree *lchild;
struct tree *rchild;
struct tree *parent;
int color;
}RBtree;
RBtree *Create_tree(){
Elemtype num;
RBtree *t = NULL;
while(scanf("%d",&num),num){
t = Insert_Elem(t,num);
}
return t;
}
三.红黑树的基本操作之旋转
旋转操作分为左旋和右旋,左旋是将某个节点旋转为其右孩子的左孩子,而右旋是节点旋转为其左孩子的右孩子。
左右旋转示意图:
上图包含了左旋和右旋的示意图,这里以右旋为例进行说明,右旋节点 M 的步骤如下:
- 将节点 M 的左孩子引用指向节点 E 的右孩子
- 将节点 E 的右孩子引用指向节点 M,完成旋转
附上当时学习红黑树写的旋转代码:
RBtree *Single_left(RBtree *t,RBtree *x){
RBtree *y = x->rchild;
x->rchild = y->lchild;
if(y->lchild){
y->lchild->parent = x;
}
y->parent = x->parent;
if(!x->parent){
t = y;
}
else{
if(x->parent->lchild == x){
x->parent->lchild = y;
}
else{
x->parent->rchild = y;
}
}
y->lchild = x;
x->parent = y;
return t;
}
RBtree *Single_right(RBtree *t,RBtree *x){
RBtree *y = x->lchild;
x->lchild = y->rchild;
if(y->rchild){
y->rchild->parent = x;
}
if(!x->parent){
t = y;
}
else{
if(x->parent->lchild == x){
x->parent->lchild = y;
}
else{
x->parent->rchild = y;
}
}
y->rchild = x;
x->parent = y;
return t;
}
四.红黑树的基本操作之添加元素
红黑树的插入过程和二叉查找树插入过程基本类似,不同的地方在于,红黑树插入新节点后,需要进行调整,以满足红黑树的性质。性质1规定红黑树节点的颜色要么是红色要么是黑色,那么在插入新节点时,这个节点应该是红色还是黑色呢?答案是红色,原因也不难理解。如果插入的节点是黑色,那么这个节点所在路径比其他路径多出一个黑色节点,这个调整起来会比较麻烦(参考红黑树的删除操作,就知道为啥多一个或少一个黑色节点时,调整起来这么麻烦了)。如果插入的节点是红色,此时所有路径上的黑色节点数量不变,仅可能会出现两个连续的红色节点的情况。这种情况下,通过变色和旋转进行调整即可,比之前的简单多了。
现在我们来分析一下新增的节点(红色)插入之后可能面临的几种情况,以及他们的处理措施:
1.插入的节点为根节点
新插入的红色节点变成黑色节点,满足根节点为黑色节点的要求
2.父亲节点为黑色节点
这个时候不需要进行任何调整操作,此时的树仍然是一颗标准的红黑树
3.父亲节点为红色节点的情况下,叔叔节点为红色节点(不用考虑左右)
解决方案:将叔叔和父亲节点改为黑色,爷爷节点改为红色,然后又将爷爷节点当作插入节点看待,一直进行上面的操作,直到当前节点为根节点,然后将根节点变成黑色。
4.父亲节点为红色,叔叔节点为黑色
1)父亲节点为爷爷节点的左孩子,新插入节点为父节点的左孩子(左左)
解决方案:将父亲节点和爷爷节点颜色互换(父节点变为黑色,爷爷节点变为红色),然后对爷爷节点进行一次右旋
注:上图叔叔是空叶子节点,所以也是黑色
2)父亲节点为爷爷节点的右孩子,新插入节点为父节点的右孩子(右右)
解决方案:将父亲节点和爷爷节点颜色互换(父节点变为黑色,爷爷节点变为红色),然后对爷爷节点进行一次左旋
3)父亲节点为爷爷节点的左孩子,新插入节点为父节点的右孩子(左右)
解决方案:对父亲节点进行一次左旋,然后就变成了情况1,按照情况1再进行处理
4)父亲节点为爷爷节点的右孩子,新插入节点为父节点的左孩子(右左)
解决方案:对父亲节点进行一次右旋,然后就变成了情况2,按照情况2再进行处理
附上这一段插入元素后修复红黑树的代码:
RBtree *Create_tree(){ //创建一棵二叉排序树
Elemtype num;
RBtree *t = NULL;
while(scanf("%d",&num),num){
t = Insert_Elem(t,num);
}
return t;
}
RBtree *Insert_Elem(RBtree *t,Elemtype num){
//二叉排序树的插入方式,在尾部放一个修复红黑树的函数
RBtree *y = NULL;
RBtree *x = t;
RBtree *p = (RBtree *)malloc(sizeof(*p));
p->color = RED;
p->data = num;
p->lchild = p->rchild = p->parent = NULL;
while(x){
y = x;
if(x->data > num){
x = x->lchild;
}
else{
x = x->rchild;
}
}
p->parent = y;
if(y){
if(y->data > num){
y->lchild = p;
}
else{
y->rchild = p;
}
}
else{
t = p;
}
t = Fixup(t,p);
return t;
}
RBtree *Fixup(RBtree *t,RBtree *n){
RBtree *p = NULL; //父节点
RBtree *gp = NULL; //祖节点
while((p = n->parent) && !p->color){ //他父亲存在且为红色
gp = p->parent;
if(p == gp->lchild){ //父节点为祖节点左孩子
RBtree *uncle = gp->rchild; //叔叔节点
if(uncle && !uncle->color){ //如果叔叔存在且为红色
change_color(uncle); //叔叔变色
change_color(p); //父节点变色
change_color(gp); //祖节点变色
n = gp; //换成祖节点继续往上遍历
continue;
}
if(p->rchild == n){ //叔叔为黑或不存在,左右
t = Single_left(t,p); //父节点左旋
RBtree *tmp = p;
p = n; //他变成新的父节点
n = tmp; //父节点旋转后变成子节点
}
//此时变成他叔叔不存在或为黑,左左
change_color(p); //父节点变色
change_color(gp); //祖节点变色
t = Single_right(t,gp); //祖节点右旋
}
else{ //父节点为祖节点右孩子
RBtree *uncle = gp->lchild;
if(uncle && !uncle->color){ //如果叔叔存在且为红色
change_color(uncle); //叔叔变色
change_color(p); //父节点变色
change_color(gp); //祖节点变色
n = gp; //换成祖节点继续往上遍历
continue;
}
if(p->lchild == n){ //他为祖节点左孩子,右左
t = Single_right(t,p); //父节点右旋
RBtree *tmp = p;
p = n; //旋转后,左子成为新的父节点
n = tmp; //原父节点变为右子
}
//此时变成右右
change_color(p); //父节点变色
change_color(gp); //祖节点变色
t = Single_left(t,gp); //祖节点左旋
}
}
if(!t->color) change_color(t); //根变黑
return t;
}
五.红黑树的基本操作之删除元素
相较于插入操作,红黑树的删除操作则要更为复杂一些。删除操作首先要确定待删除节点有几个孩子,如果有两个孩子,不能直接删除该节点。而是要先找到该节点的前驱(该节点左子树中最大的节点)或者后继(该节点右子树中最小的节点),然后将前驱或者后继的值复制到要删除的节点中,最后再将前驱或后继删除。由于前驱和后继至多只有一个孩子节点,这样我们就把原来要删除的节点有两个孩子的问题转化为只有一个孩子节点的问题,问题被简化了一些。
删除一个节点有以下四种情况:
1.删除的节点没有孩子
2.删除的节点只有左子树
3.删除的节点只有右子树
*4.删除的节点拥有左子树和右子树
其实只有上面前三种情况,对于第四种情况,可以找到待删除节点的直接后继节点,用这个节点的值替代待删除节点,接着情况转变为删除这个直接后继节点,情况也变为前三种之一。
1.删除的节点只有左子树或只有右子树
只有上面两种情况会存在于红黑树中,直接用DL/DR的元素值代替D的元素,再把DL/DR直接删去就好。
2.删除的节点没有孩子
1)待删除节点是红色的,直接删去这个节点。
2)父节点P是红色节点
解决方案:把P节点染成黑色,兄弟节点染成红色,删除节点D。
3)兄弟节点S是红色节点
解决方案:把P染成红色,S染成黑色,然后以P为轴做相应的旋转操作(D为P的左子树则左旋,否则右旋),变成了情况2(父节点为红色),按照情况2进行操作。
4)节点D的远亲侄子为红色节点的情况(父节点P可红可黑)
解决方案:交换P和S的颜色,然后把远亲侄子节点SR/SL设置为黑色,再已P为轴做相应的旋转操作(D为P的左子树则左旋,否则右旋),删除节点D。
5)节点D的近亲侄子为红色节点的情况(父节点P可红可黑)
解决方案:把S染成红色,把近亲侄子节点SR/SL染成黑色,然后以节点S为轴做相应的旋转操作(D为P的左子树则右旋,否则左旋),变成了情况4,按照情况4进行操作。
6)节点D,P,S均为黑色节点
解决方案:把D删去,然后把节点S染成红色。
①从节点P往上依然是全黑的情况
②从节点P往上是其他情况
删除节点及修复红黑树代码:
RBtree *Find_Elem(RBtree *t,Elemtype num){
RBtree *b = t;
while(b){
if(b->data > num){
b = t->lchild;
}
else if(b->data < num){
b = b->rchild;
}
else{
return b;
}
}
return NULL;
}
RBtree *Del_Elem(RBtree *t, Elemtype num){
RBtree *n = Find_Elem(t,num);
RBtree *child = NULL;
RBtree *parent = NULL;
int color;
if(n->lchild && n->rchild){
RBtree *m = n->rchild;
while(m->lchild){
m = m->lchild;
}
if(n == t){
t = m;
}
else{
if(n->parent->lchild == n){
n->parent->lchild = m;
}
else{
n->parent->rchild = m;
}
}
parent = m->parent;
child = m->rchild;
color = m->color;
if(m->parent->lchild == m){
m->rchild = n->rchild;
m->parent->lchild = child;
if(child)
child->parent = m->parent;
}
m->parent = n->parent;
m->color = n->color;
if(n->lchild){
m->lchild = n->lchild;
n->lchild->parent = m;
}
n->lchild = n->rchild = n->parent = NULL;
free(n);
if(color){
t = Del_fixup(t,child,parent);
}
return t;
}
parent = n->parent;
color = n->color;
if(n->lchild){
child = n->lchild;
}
else if(n->rchild){
child = n->rchild;
}
if(child){
child->parent = parent;
}
if(parent){
if(parent->lchild == n){
parent->lchild = child;
}
else{
parent->rchild = child;
}
}
else t = child;
if(color){
t = Del_fixup(t,child,parent);
}
n->lchild = n->rchild = n->parent =NULL;
free(n);
return t;
}
RBtree *Del_fixup(RBtree *t,RBtree *n,RBtree *p){
RBtree *bro = NULL;
while((!n || n->color) && n != t){
if(n == p->lchild){
bro = p->rchild;
if(!bro->color){ //兄弟为红色,情况三
b_color(bro);
r_color(p);
t = Single_left(t,p);
bro = p->rchild;
}
if((!bro->lchild || bro->lchild->color) && (!bro->rchild || bro->rchild->color)){ //兄弟为黑,且兄两个儿子为黑
r_color(bro); //情况二的话,兄弟变红,更新n为父节点
n = p; //因为父节点为红,跳出循环走最后一步
p = n->parent;
}
else{
if(!bro->rchild || bro->rchild->color){
//兄弟黑,孩子右黑左红
b_color(bro->lchild); //情况五
r_color(bro);
t = Single_right(t,bro);
bro = p->rchild;
}
bro->color = p->color; //兄弟黑,孩子右红左任意
b_color(p); //情况四
b_color(bro->rchild);
t = Single_left(t,p);
n = t;
break;
}
}
else{
bro = p->lchild;
if(!bro->color){ //兄弟为红
b_color(bro);
r_color(p);
t = Single_right(t,p);
bro = p->lchild;
}
if((!bro->lchild || bro->lchild->color) && (!bro->rchild || bro->rchild->color)){ //兄弟为黑,且兄两个儿子为黑
r_color(bro);
n = p;
p = n->parent;
}
else{
if(!bro->lchild || bro->lchild->color){
//兄弟黑,孩子左黑右红
b_color(bro->rchild);
r_color(bro);
t = Single_left(t,bro);
bro = p->lchild;
}
bro->color = p->color; //兄弟黑,孩子左红右任意
b_color(p);
b_color(bro->lchild);
t = Single_right(t,p);
n = t;
break;
}
}
}
if(n){
b_color(n); //只有左或右子树直接走这步
}
return t;
}
文章只作自己日后参考复习