彻底了解红黑树

来源:互联网 发布:查询域名是不是被降权 编辑:程序博客网 时间:2024/06/08 10:38

 首先声明:这是本人第一次写一篇关于算法的研究的文章,也是打算发表的系列文章的第一篇。由于本人能力有限,所以如有不妥或是错误之处,非常欢迎大家给予指导!本人不胜欢迎!学习贵在分享!

真是对不起,浏览器不支持画的图,截图又太麻烦,所以有需要的可以联系我,我把原文发给你,本人QQ:1447932441

彻底了解红黑树(一种特殊的二叉查找树):

一、红黑树的四点重要性质:

1、        每个结点或是红的,或是黑的

2、        根结点是黑的

3、        如果一个结点是红的,则它的两个儿子是黑的。

4、        对每个结点而言,从该结点到其子孙结点的所有路径上包含相同数目的黑结点

二、需要注意的定义:

1、红黑树的定义:只有满足以上四点的二叉查找树才为红黑树(其实还有第五点,但是对于我们去学习红黑树没有太大的帮助(个人认为)所以那个就不给于考虑了)

2、从某个结点x出发(不包括该节点)到达一个叶节点的任意一条路径上,黑色结点的个数称为该结点x的高度。

3、结构体的定义(将数据定义为整数型,大家需要的可以自己更改)

const int R = 1;

const int B = 0;

//红黑树结构体

typedef structRB_TREE

{

       Int color;

       int data;

       RB_TREE *left, *right;

    RB_TREE *parent;

}*RB_T;

三、介绍完红黑树的一些基本情况以后,我们来一一具体介绍红黑树的一些基本操作吧:

1、红黑树的旋转:

旋转操作包括左旋和右旋:

左旋代码:

//左旋操作

voidleft_rotate(RB_T &T, RB_T x)

{

       RB_T y = x->right;

       x->right = y->left;

       if(y->left)

              y->left->parent = x;

       y->parent = x->parent;

       if(x->parent == NULL)

              T = y;

       else

       {

              if(x == x->parent->left)

                     x->parent->left = y;

              else x->parent->right = y;

       }

       y->left = x;

       x->parent = y;

}

 

                        

右旋代码:

//右旋操作

voidright_rotate(RB_T &T, RB_T x)

{

       RB_T y = x->left;

       x->left = y->right;

       if(y->right)

              y->right->parent = x;

       y->parent = x->parent;

       if(x->parent == NULL)

              T = y;

       else

       {

              if(x == x->parent->right)

                     x->parent->right = y;

              else x->parent->left = y;

       }

       y->right = x;

       x->parent = y;

}

 

对旋转操作的分析:

旋转操作分析:图中a,b,c均为子树,由图可以看出,左旋操作的前提该结点是要有右孩子,而右旋操作的前提是该结点要有左孩子。并且大家应该发现了,旋转操作全部都是通过指针实现的。所以其时间复杂度只是O(1)而已。具体中子树的变化,可以看上图以及代码

2、插入:

红黑树的插入操作是指,在一棵已经平衡的红黑树上再插入一个结点,并将插入结点的颜色定义为红色,在插入之后经过一系列的转化之后仍旧是平衡的。因为在插入过程中可能会导致红黑树的性质被破坏。

插入由总共六种情况,可根据树的形态大概分为两组,分别为两两对称,所以我只分析前三种情况画图并作出解释,后三种就只是给出相关代码,有兴趣的可以自己看看。

前三种是以插入结点的双亲结点为其祖父结点的左孩子,后三种则是以插入结点为其双亲的右孩子。假设插入结点标记为z

前三种

情况一:

 

 

第一种情况分析:z的叔叔是红色的,z为插入的结点。这种情形中z是其父亲结点的左孩子还是右孩子都没有影响,效果是一样的(由上图可以看出)。只有这种情况会进入while循环,因为此种情况只是将指针z上移两层而已,把z->parent->parent当做新增的结点z来处理。经过while的循环,将该情况转化为其他的情况处理

通过看代码,大家会更加清晰:

//case  1

y->color= B;

z->parent->color= B;

z->parent->parent->color= R;

z =z->parent->parent;

 

 

 

情况二分析:此情况就是通过一次右旋操作,将情况二变为情况三。做统一处理。值得注意注意的是,做右旋的是z->parent即双亲。

具体的可以通过代码了解:

if(z ==z->parent->right)

{

z =z->parent;                                                                                                                                                                                                                                                                

left_rotate(T,z);

}

 

情况三分析:若没有情况二,则直接进入情况三。通过改变z的双亲及其祖父的颜色,再以其祖父为中心将该子树进行一次右旋操作。变换后该子树范围内符合了红黑。

情况三的代码:

z->parent->color= B;

z->parent->parent->color= R;                                                                              

right_rotate(T,z->parent->parent);

与以上三种情况对称的是z->prent == z->parent->parent->right的三种情况,基本思想与图形都一样,看代码即可,这里就不再赘述了。

 

关于红黑树的插入操作还有几点需要注意的:

以上三种情况中其祖父必定为黑色的,因为z是红色的,z->parent也是红色的,而此前的红黑树是满足红黑性质的,所以其祖父必为黑色。

经过上面的叙述,有人可能还会有疑问:在各种情况中要是插入结点z的叔叔结点为空呢?

这个其实大家从代码中应该就能够知道一二了,这里的代码是经过我调试的,其实一开始我也没有考虑这种情况,但是在我调试的过程中,有几种情况就是通不过,我才发现这种情况。所以在这里将这个抽出来特地说明一下。

给大家举一个例子比如你要插入32,3,2三个数据,这三个数据每次插入其叔叔都是空的,这样要怎么解决呢?请大家仔细想一想,在上述的情况三,是不是没有再设计其叔叔的代码了,我们就假设y是空的或是黑的,只要它不影响红黑性质即可。对吧。所以呢,当y为空的时候,我们只需要在前几种情况的判断中加入判断y是否为空即可,判断为空,我们不做处理,交给后面对y不做要求的情况处理即可。这下大家应该就能理解其中的原因了吧。

各情况都分析完了,现在提供一下总的代码吧:

 

///对违反红黑性质的插入操作进行修复

void RB_INSERT_FIXUP(RB_T &T,RB_T z)

{while(z->parent && z->parent->color== R && z->parent->parent)

                                                                                                                                    {

//此处必须判断一下z->parent是否为空,因为经过循环z->parent不确定是否会存在,必须注意这一点

RB_T y = NULL;

if(z->parent == z->parent->parent->left)

{y = z->parent->parent->right;

if(y && y->color == R)

{

//情况一:唯一可能进入下次while的情况

y->color = B;

z->parent->color = B;

z->parent->parent->color = R;

z = z->parent->parent;

}else {

if(z == z->parent->right)//若有情况二,则将情况二转化为情况三

{

z = z->parent;//此处必须这么做,否则将会出现与情况三不相符的现象

left_rotate(T,z);

}

//直接进入情况三:

z->parent->color = B;

z->parent->parent->color = R;

right_rotate(T, z->parent->parent);

}}else //z->parent ==z->parent->parent->right

{

y =z->parent->parent->left;

if(y&& y->color == R)//情况一

{y->color= B;

z->parent->color= B;

z =z->parent->parent;}

else {

if(z ==z->parent->left)//若有情况二,则将情况二转化为情况三

{

z = z->parent;//此处必须这么做,否则将会出现与情况三不相符的现象

right_rotate(T,z);

}

//直接进入情况三:

z->parent->color= B;

z->parent->parent->color= R;

left_rotate(T,z->parent->parent);

}}}

T->color= B;

}

 

//插入操作

voidRB_T_INSERT(RB_T &T, RB_T z)

{

RB_T y =NULL;//插入结点的双亲结点

RB_T x = T;  

while(x)

{

y = x;

if(x->data> z->data)

x =x->left;

else x =x->right;

}

z->parent= y;

if(y ==NULL)

{

T = z;

}

else

{

if(z->data< y->data)

y->left =z;

elsey->right = z;

RB_INSERT_FIXUP(T,z);

}

}

 

3、红黑树的删除操作:

红黑树的删除操作是通过给定一点,将其从红黑树上剪下。并通过一系列变化保持红黑树性质的一组操作。

删除操作其实有三种情况:

1、        该结点的左右孩子都为空,该种情况可以直接删除该结点。

2、        该结点有左或右孩子,则可以通过上移左右孩子完成删除操作

3、        该结点有左孩子也有右孩子。这种情况有两种办法,一种就是通过其后继代替该结点,另一种就是通过其前驱代替该结点。我们采用前者。

红黑树的删除操作比红黑树的插入操作要相对复杂一些。删除后修复可以分为两组,每组中有四种情况。两组分别是左右对称的,这里和插入操作一样,只分析第一组的四种情况,后面的四种只是给出代码,有兴趣的可以自己看看。

首先分析一下删除操作会如何破坏红黑树的性质:

为了分析方便,假定删除的结点为y,删除y后其的一个孩子上移,假设该孩子为x:

1、        如果y原来是根结点,而y的一个红色的孩子成为了新的根,这就违反了红黑树的根必须为黑色的性质。

2、        如果xy->parent(现在也是x的双亲),都是红的,就违反了红黑树中红色结点有两个黑色孩子的性质。

3、        删除y将导致先前包含y的任何路径上黑色结点的个数少了1。因此红黑树的性质被y的其中一个祖先破坏了。

为了解决这个问题的一个办法就是给y的上移的孩子x额外再着一重黑色。这样就可以弥补因为删除y而损失的一重黑色。这就相当于x此时有了两种颜色,x原本是黑色,现在就是双重黑色,要是原本是红色,那么现在就是红黑色。

虽然我们自己额外给x定义了一种黑色,用来平衡因为删除y而导致红黑性质被破坏的红黑树。但是这样只是从概念上平衡了红黑树,没有真正的平衡红黑树,要想真正的平衡红黑树,我们就必须把我们概念上定义的x的双重色重新变为一重。并且保持前后红黑性质不变。

在未分析几种删除的情况时,给大家留一个问题,希望大家在看我的分析过程中可以思考一下。

问题就是:在每种情况中,从(且包括)子树的根到每棵子树a,b,c…….之间的红黑性质4是如何保持的?

 

在介绍了删除的一些前提事件后,现在就正式分析一下各种删除的情况了,并通过一定的变换将x的双重颜色去除。

对图形的特殊说明:灰色是指,对该树的颜色不需要确定,即可以为黑色也可以为红色,没有影响,只要前后一致即可。a,b,c….依然代表子树。

 

 

情况一分析:

结点A为双重色结点x,所以可以知道,该结点B的左子树该阶段的黑色结点数目为2,所以必须一边将x的双重色去除,又不能改变期间的黑色结点的数目,所以以x->parent为轴进行左旋一次。之后进入情况二、三、四。

情况一代码:

if(w->color== R)

{

w->color= B;

x->parent->color= R;

left_rotate(T,x->parent);

w =x->parent->right;

}

 

 

 

 

情况二分析:

唯一一个有可能进入while循环的一种可能,此操作不改变该阶段子树的结构,通过改变指针x的指向,将x上移两层,此时为B结点额外着了一重黑色。从而进入下一次while循环。

情况二代码:

if(w->left->color == B &&w->right->color == B)

{

w->color = R;

x = x->parent;

}

 

情况三分析:

该种情况,是向情况四过度的,如果存在该种情况就通过适当的颜色变化和一次右旋将其变成情况四,在情况四中统一处理。

情况三代码:

if(w->right->color== B)

{

w->left->color= B;

w->color= R;

right_rotate(T,w);

w =x->parent->right;

}

 

 

 

 

情况四分析:

A结点为双色结点,a,b子树该阶段的黑色结点个数为2+(因为D的颜色不确定),所以必须通过变换适当结点的颜色,并对以x->parent为旋转结点进行左旋操作。这种情况中值得注意一点的是最后一个语句,将x指针指向树根。这么做的原因是经过情况四的处理,该子树已经平衡了,通过指向树根带入while循环结束。

情况四代码:

w->color = x->parent->color;

x->parent->color = B;

w->right->color = B;

left_rotate(T, x->parent);

x = T;

四种情况分析完了,再加上另外的四种情况的总的代码,一起给出:

//删除操作的修复

void RB_T_DELETE_FIXUP(RB_T &T, RB_T x)

{while(x != T && x->color == B)

{RB_T w;

if(x == x->parent->left){

w = x->parent->right;

if(w->color == R)

{w->color = B;

x->parent->color = R;

left_rotate(T, x->parent);

w = x->parent->right;}

if(w->left->color == B &&w->right->color == B)

{w->color = R;

x = x->parent;}

else {

if(w->right->color == B)

{w->left->color = B;

w->color = R;

right_rotate(T, w);

w = x->parent->right;}

w->color = x->parent->color;

x->parent->color = B;

w->right->color = B;

left_rotate(T, x->parent);

x = T;}

}else{//x == x->parent->right

w = x->parent->left;

if(w->color == R)

{w->color = B;

x->parent->color = R;

right_rotate(T, x->parent);

w = x->parent->left;

}

if(w->left->color == B &&w->right->color == B)

{

w->color = R;

x = x->parent;

}else {

if(w->left->color == B){

w->right->color = B;

w->color = R;

left_rotate(T, w);

w = x->parent->left;}

w->color = x->parent->color;

x->parent->color = B;

w->left->color = B;

right_rotate(T, x->parent);

x = T;

}}}}

//删除操作

void RB_T_DELETE(RB_T &T, RB_T z)

{

RB_T x, y;//y为删除结点,x为删除结点的孩子(左孩子或是右孩子)

if(z->left == NULL || z->right == NULL)                                                                   

y = z;

else y = successor(z);

if(y->left)

x = y->left;

else x = y->right;

if(x) x->parent = y->parent;

if(y->parent == NULL)T = x;

else {                                                                                                                                

if(y == y->parent->left)

y->parent->left = x;

else y->parent->right = x;

}

if(y != z)

{int t = y->data;

y->data = z->data;

z->data = t;}

if(y->color == B)

RB_T_DELETE_FIXUP(T, x);

}

先前给大家留的问题,大家应该还能够记得,我想大部分人经过这几种情况的分析应该从我的叙述中明白了。但可能也有人还不明白。

这里就解释一下:首先一点,通过对x结点额外着一重黑色,不考虑其他原因的话,该树已经是一棵“正常”的红黑树了。我们所做的四种情况仅仅只是将x的另一重黑色去除而已。大家看看各个子树a,b,c….,这些子树无论是变换前还是变换后,该段路径中,其黑色结点的数目都不会发生变化,所以这么看的话,该段路径变换前后的红黑性质没有发生变化。

还有值得注意一点的就是和插入一样的以为,其兄弟为空的情况,这个我也不想再解释了,只是提醒大家一下,不要钻牛角尖。

 

红黑树的题外话:

再次声明一下,本人不是什么大牛,只是因为喜欢算法,将自己喜欢的东西和大家一起探讨一下,共同学习而已。

还有就是提一点,本人感觉(也仅仅是本人观点)算法这东西不能过于钻牛角尖,虽然本人就是有一点这毛病,但是真的不能太钻牛角尖,打比方说,红黑树的插入删除的各种情况的操作,你非得问我,为什么这么做?我可以回答你,我也不知道。为什么这么做是通过很多很多人称“教授“的大人物想出来的,我们做的就是领会他们的思想。没有必要非得钻牛角尖,是吧。

这些纯属废话,略过不读,哈哈