学习红黑树

原文链接http://blog.csdn.net/lyh__521/article/details/49909953

什么是红黑树?

  这个装逼的名字可能挫败过很多初学者。至少我在听过很多次它的大名之后,知道了红黑树怎么左旋、右旋,却仍然不知道红黑到底是个啥。
  其实,红黑树就是一棵二叉查找树,只是他是改良过的树。这棵树在生长的过程中,我们给加入了限制条件,不允许某些树枝疯狂增长,而是要各方面均衡发展。于是,我们给二叉查找树结点增加了一个颜色域,就是所谓的红/黑,说白了就是一个标志位,完全可以把它的取值(红和黑)改为(0和1)。


关于二叉查找树(插入、删除、求后继等后文会用到)可以参考:http://blog.csdn.net/lyh__521/article/details/49811149

这就是一棵红黑树:N 表示NULL结点

这里写图片描述

红黑树结点的域

域就是结构体/类的成员

含义
color结点的颜色
key结点的关键字
left指向左孩子
right指向右孩子
parent指向父结点

为什么要有红黑树?

   n个结点的二叉查找树的平均操作时间是Olg(n),但是当我们将一组递增的数据插入到二叉查找树中时,就很容易产生下面这种情况:

这里写图片描述

经过不断的删除、插入操作后,二叉查找树会越来越接近单枝的形状了,变得和链表没多大区别了,时间复杂度变成了最坏的情况O(n)。此时,二叉查找树还有什么优势可言呢。

  于是引入了红黑树,在树的生长过程中,通过一些约束条件的限制,使的一棵有 n 个结点的红黑树,其最长的路径最多不会大于最短路径的两倍,树的高度最大为 2lg(n+1),所以时间复杂度最差也是Olg(n)。

哪里用到了红黑树?

  红黑树(RBT)是一种自平衡二叉查找树,他的统计性能要好于平衡二叉树(AVL)。红黑树在很多地方都有应用。在C++ STL中,很多部分(包括set, multiset, map, multimap)应用了红黑树的变体(SGI STL中的红黑树有一些变化,这些修改提供了更好的性能,以及对set操作的支持)。

红黑树的性质


5条基本性质


1、 每个结点要么是黑的,要么是红的。
2、 根结点是黑的。
3、 NULL结点都是黑的。

这里得强调一下,《算法导论》以及网上很多文章给的定义是“每个叶结点(NULL)都是黑的”。估计这个定义得让很多初学者茫然,因为树中对于叶子结点的定义是“度数为0(即没有孩子)的结点叫叶子结点”。那这里说的叶结点和NULL结点岂不矛盾了?实际上,为了容易操作红黑树,红黑树中的NULL结点显尤为重要,一般考虑问题的时候不可忽略它,特别的把NULL结点称为叶结点。所以在红黑树中说到的叶子结点指的都是NULL结点。


4、 如果一个结点是红的,那么他的两个孩子结点都是黑的。
5、 对每个结点,从该结点到每个叶子(NULL)结点的所有路径上都包含相同数目的黑结点。

性质4和5特别重要,加上这两条性质就能让红黑树接近平衡。在1、2、3的前提下,由性质4确定一条路径上不会存在连续的红结点,所以要想让一条路径尽可能的长,就必须是红黑交错排列的,黑结点占这条路径上结点数的一半,而要想让一条路径尽可能的短就不能包含红结点;由性质5确定,即便是最短的路径也要与最长路径有相同数目的黑结点,所以说明最短路径的黑结点至少是最长路径结点数的一半,因此,红黑树最长路径最大不会大于最短路径的2倍,约束了红黑树的平衡性。


红黑树的黑高和树高

  1. 黑高度
      从某个结点x出发(不包括该结点)到达一个叶结点的任意一条路径上,黑色结点的个数称为该结点x的黑高度,简称黑高。红黑树的黑高度为根结点的黑高度。叶结点(NULL)的黑高度为0。

  2. 一棵有n个内结点(关键字有值,即不含NULL的结点)的红黑树的高度 h 至多为 2lg(n+1)。所以时间复杂度为 O(h) = Olg(n) .

下面我们用归纳法证明(不感兴趣可以跳过,看看其实很简单)
证明:
bh(x)表示x结点的黑高,h 表示树高。
我们假设以某一结点x为根的子树中至少包含 2bh(x)1 个内结点。如果 x 高度为0(空树),该树包含 201 个内结点,符合假设。如果 x 不是空树,那么他的孩子为红色或黑色,如果孩子是黑色,则孩子对应的子树的黑高=bh(x)-1 ; 如果孩子是红色,则孩子对应的子树的高度=bh(x) 。那么,根据归纳假设,x 的一个孩子至少包含 2bh(x)11 个内结点。这样,以 x 为根的树至少包含( 2bh(x)11 )+( 2bh(x)11 )+1 = 2bh(x)1 个内结点。符合假设,证得结论:以某一结点x为根的子树中至少包含 2bh(x)1 个内结点。
根据性质4,从根到叶子结点的一条简单路径上至少有一半的黑结点,所以,根的黑高至少是 h/2 。所以内结点数 n 2h/2 -1,然后把1移到左边,两边取对数,得: h 2lg(n+1)
证毕!

红黑树的旋转

因为红黑树的5条规则太严格,红黑树插入、删除结点时,很容易破坏这5条规则,这时利用旋转操作可以调整某些结点的位置,使得红黑树重新符合这5条规则。后面的插入和删除操作时会使用。

注: ABC 表示子树或者NULL


左旋转

这里写图片描述

  当某个结点 x 的右孩子 y 不为NULL时,我们可以对结点 x 做左旋转,使得 y 成为该子树新的根,x 成为 y 的左孩子,而 y 原来的左孩子成为 x 的右孩子。

左旋转以 x 到 y 之间的链为“支轴”进行,如上图轴xy 逆时针旋转成为 y 的左孩子,轴By 逆时针旋转成为 x 的右孩子,图中的虚线表示旋转后不存在。

void Left_Rotate(RB_Tree* root,RB_Tree* x)
{
    RB_Tree* y = x->right;                //y 指向x的右子树
    x->right = y->left;                   //把y的左孩子变为x的右孩子
    if(y->left != NULL)                   
        (y->left)->parent = x;            //设置x为y左孩子的父亲
    y->parent = x->parent;                //y移到x原来的位置
    if(x->parent == NULL)                  //如果x原来是根
        root = y;                         //重新设置y为新的根
    else if(x == (x->parent)->left)        //否则如果x原来是左树
        (x->parent)->left = y;            //设置y为左子树
    else
        (x->parent)->right = y;           //设置y为右子树
    y->left = x;                          //设置y的左孩子为x
    x->parent = y;                        //设置y是x的父结点
}

右旋转

这里写图片描述

右旋转是左旋转的逆操作,如上图,只需要将轴xy 和轴Bx 按顺时针旋转到对应位置就好了。只不过右旋转称作对 y 旋转。

根据上面,我们可以发现:子树经过左旋转或者右旋转之后,虽然某些结点的位置改变了,但是子树依然保持了关键字的有序性,左子树值小,右子树值大。如上,无论是旋转前还是旋转后,中序遍历都是 AxByC 这就是旋转操作的妙用。


插入

  红黑树首先是一棵二叉查找树,所以红黑树插入一个结点时,我们首先完全按照二叉查找树的插入方法来往红黑树插入结点。然后再通过旋转和对部分结点重新着色来让树重新符合红黑树的性质。

1、 先用二叉查找树的方法插入一个新结点 z

//插入
void RB_Insert(RB_Tree* root,RB_Tree* z)
{
    RB_Tree*    p = NULL; //指向插入结点的父结点
    RB_Tree*    x = root;
    while(x != NULL)
    {
        p = x;
        if(z->key < x->key)
            x = x->left;
        else
            x = x->right;
    }
    z->parent = p;
    if(p == NULL)
        root = z;
    else if(z->key < p->key)
        p->left = z;
    else
        p->right = z;
    z->left = NULL;
    z->right= NULL;
    z->color= RED;
    RB_Insert_FIXUP(root,z);
}

结点插入后,将新插入的结点置为红色,因为如果将结点置为黑色,对应路径就会多出一个黑结点,违反了性质5,违反了性质5是很难调整的。

2、 调整

为了方便描述,我们将父结点的父结点称为爷爷结点,父结点的兄弟结点称为叔叔结点

新增结点 z 后,主要会出现4类(准确的说是7种)情况,后3种需要调整

1)

  • 情况 1z 的父亲是黑色

这里写图片描述

新插入了结点40,此时没有破坏任何一条规则,无需调整。父亲是红色时才需要调整,如下。

2)z 的父亲是红色,并且 z 的父亲是左孩子

  • 情况 2z 的父亲是红色,z 的叔叔 y 是红色的

这里写图片描述

如上图,这种情况破坏了性质4,出现了两个连续的红色,父亲是红色则爷爷必为黑色(性质4),所以我们将父亲和爷爷交换颜色,即父亲设置黑色,爷爷设置红色,再设置叔叔 y 为黑色(为了不破坏性质5),最后将 z 指向爷爷。此时,如果符合5条性质则调整完成,否则将爷爷看作新的 z (可能变为2,3,4任意一种情况),继续调整。

  • 情况 3z 的父亲是红色,z 的叔叔 y 是黑色的,并且 z 是右孩子

这里写图片描述
这里写图片描述

这是最差的情况了,看起来无从下手。我们将 z 指向父亲,之后左旋 z ,这样就可以转变为情况4了。

  • 情况 4z 的父亲是红色,z 的叔叔 y 是黑色的,并且 z 是左孩子

这里写图片描述

这是最好的情况。此时将父亲置为黑色,爷爷置为红色,右旋爷爷就OK了。调整完成。

3)

2)种的3种情况是 z 的父亲的左孩子,还有对称的另外3种情况,即 z 的父亲是右孩子。只需要将各情况中对应的左换为右,左旋换为右旋就可以了。

具体看代码:


void RB_Insert_Fixup(RB_Tree* root,RB_Tree* z)
{
    while((z->parent)->color == RED)                  //如果父亲是红色需要调整
    {
        if(z->parent == (z->parent->parent)->left)    //父亲是左孩子
        {
            y = (z->parent->parent)->right;           //y 每次都指向叔叔
            if(y->color == RED)                       //如果叔叔是红色
            {
                (z->parent)->color = BLACK;           //设置父亲为黑色
                y->color = BLACK;                     //设置叔叔 y 为黑色
                (z->parent->parent)->color = RED;     //设置爷爷为红色
                z = z->parent->parent;                //z 指向爷爷
            }
            else                                      //如果叔叔为黑色
            { 
                if(z == (z->parent)->right)           //z 是右孩子
                {
                    z = z->parent;                    //z 指向父亲
                    Left_Rotate(root,z);              //左旋父亲
                }
                (z->parent)->color = BLACK;           //设置父亲为黑色
                (z->parent->parent)->color = RED;     //设置爷爷为红色
                Right_Rotate(root,z->parent->parent); //右旋爷爷
            }
        }
        else                                          //如果父亲是右孩子
        {
            y = (z->parent->parent)->left;            //y 指向叔叔
            if(y->color == RED)                       //如果叔叔是红色
            {
                (z->parent)->color = BLACK;           //设置父亲为黑色
                y->color = BLACK;                     //设置叔叔为黑色
                (z->parent->parent)->color = RED;     //设置爷爷为红色
                z = z->parent->parent;                //z 指向爷爷
            }
            else                                      //如果叔叔是黑色
            { 
                if(z == (z->parent)->left)            //如果 z 是左孩子
                {
                    z = z->parent;                    //z 指向父亲
                    Right_Rotate(root,z);             //右旋父亲
                }
                (z->parent)->color = BLACK;           //设置父亲为黑色
                (z->parent->parent)->color = RED;     //设置爷爷为红色
                Left_Rotate(root,z->parent->parent);  //左旋爷爷
            }

        }
    }
    root->color = BLACK;                              //设置根为黑色
}

删除

同插入,先用二叉查找树的删除方法删除结点,然后再调整使得重新符合红黑树的5条性质。

1、删除结点

具体分析参考二叉查找树的删除操作:

//删除结点
RB_Tree*  RB_Delete(RB_Tree* root,RB_Tree* z)
{
    if(z->left == NULL || z->right == NULL) //没孩子或有一个孩子
        RB_Tree*  y = z;
    else
        y = Tree_SUCCESSOR(z);              //z 有两个孩子时求后继
                                            //y 指向真正要删除的结点

    if(y->left != NULL)                     //如果y有左子树
        RB_Tree*  x = y->left;              //x 指向左树
    else                                    
        x = y->right;                       //x 指向右树
                                            //y没有孩子时,x=NULL

    if(x != NULL)                           //如果y有一个孩子
        x->parent = y->parent;              //设置y的孩子的父亲为y的父结点

    if(y->parent == NULL)                   //要删除的是根,必只有一个孩子
        root = x;                           //设置孩子为新的根
    else if(y = (y->parent)->left)          //否则如果y是左树
        (y->parent)->left = x;              //设置y的孩子x为左树
    else
        (y->parent)->right = x;

    if(y != z)                              //如果y是后继
        z->key = y->key;                    //用y的关键字替换z
    if(y->color == BLACK)
        RB_Delete_Fixup(root,x);            //调整
    return y;                               //返回真正删除的位置
}

2、调整

注意:当欲删除结点 z 左右孩子都不为NULL时,实际删除的 y 是 z 的后继结点。

删除操作主要会分为以下几种情况

1)如果删除结点 y 是红色,则直接删除即可,不会破坏性质。

2)如果删除结点 y 是黑色,则相应的路径会少了一个黑结点,破坏了性质5。

这里写图片描述

此时分为以下几种情况:

注:如上图,x 是原来 y 的孩子结点,p 是删除y后x的父结点,w 是兄弟结点

(1)

  • 情况0x 结点是红色

直接将x 置为黑色,弥补缺少的黑结点。调整完成。

(2)当 x 是左孩子时

  • 情况1x 结点是黑色,x 的兄弟结点 w 是红色的

这里写图片描述

此时,x 的父亲必为黑色。左旋转父亲,交换父亲与兄弟 w 的颜色,转换为其他情况(2或3或4)。

  • 情况2x 结点是黑色,兄弟 w 是黑色,并且兄弟 w 的两个孩子都是黑色的

这里写图片描述

此时,x 子树比 w 子树少一个黑结点,先将 w 置为红色,保证 x,w 子树的平衡,然后将 x 上移指向父结点,继续调整。

  • 情况3x 结点是黑色,兄弟 w 是黑色,w 的左孩子是红色,右孩子是黑色

这里写图片描述

交换兄弟 w 与左孩子颜色,右旋兄弟 w ,转为情况4。

  • 情况4x 结点是黑色,兄弟 w 是黑色,并且 w 的右孩子是红色

这里写图片描述

这是终极情况 。此时,交换 w 与 p 的颜色,左旋转父亲 P,这样可以补回左边缺失的黑色; 然后将 w 的右孩子置为黑色,这样就可以达到平衡。调整完成。

(3)当 x 是右孩子时

是(2)种的对称情况,根据(2)将相应的左孩子改为右孩子,左旋转改为右旋转即可。具体见以下代码中的 else 部分。

//删除调整
void RB_Delete_Fixup(RB_Tree* root,RB_Tree* x)
{
    while(x != root && x->color == BLACK)
    {
        //x 的父结点是左孩子
        if(x == (x->parent)->left)
        {
            w = (x->parent)->right;            //w 标记兄弟结点
            //情况1
            if(w->color == RED)                //如果兄弟w 是红色
            {                                  //父亲必为黑色
                w->color = BLACK;              
                (x->parent)->color = RED;      //交换父亲和兄弟的颜色
                Left_Rotate(root,x->parent);   //左旋父结点
                w = (x->parent)->right;        //重新用w 标记兄弟
            }
            //情况2
            //兄弟w的孩子都是黑色
            if(w->left->color == BLACK && w->right->color == BLACK)
            {
                w->color = RED;                //兄弟置为红色
                x = x->parent;                 //x标记上移
            }
            //情况3
            else
            {
                if(w->right->color == BLACK)   //兄弟的左孩子为红色,右孩子为黑色
                {
                    w->left->color = BLACK;
                    w->color = RED;            //交换w与左孩子颜色
                    Right_Rotate(root,w);      //右旋兄弟
                    w = (x->parent)->right;    //重新标记兄弟
                }
                //情况4
                w->color = (x->parent)->color; 
                (x->parent)->color = BLACK;    //交换兄弟与父亲的颜色
                w->right->color = BLACK;       //w 的右孩子置为黑色
                Left_Rotate(root,x->parent);   //左旋父亲
                x = root;                      //调整完成,break
            }       
        }
        //x 的父亲是右孩子
        else
        {
            //上面的对称情况,只需将上面的左孩子改为右孩子,左旋转改为右旋转
            w = (x->parent)->left;             //w 标记兄弟结点
            //情况1
            if(w->color == RED)                //如果兄弟w 是红色
            {                                  //父亲必为黑色
                w->color = BLACK;              
                (x->parent)->color = RED;      //交换父亲和兄弟的颜色
                Right_Rotate(root,x->parent);  //右旋父结点
                w = (x->parent)->left;        //重新用w 标记兄弟
            }
            //情况2
            //兄弟w的孩子都是黑色
            if(w->left->color == BLACK && w->right->color == BLACK)
            {
                w->color = RED;                //兄弟置为红色
                x = x->parent;                 //x标记上移
            }
            //情况3
            else
            {
                if(w->left->color == BLACK)   //兄弟的右孩子为红色,左孩子为黑色
                {
                    w->right->color = BLACK;
                    w->color = RED;            //交换w与右孩子颜色
                    Left_Rotate(root,w);      //左旋兄弟
                    w = (x->parent)->left;    //重新标记兄弟
                }
                //情况4
                w->color = (x->parent)->color; 
                (x->parent)->color = BLACK;    //交换兄弟与父亲的颜色
                w->left->color = BLACK;       //w 的左孩子置为黑色
                Right_Rotate(root,x->parent);   //右旋父亲
                x = root;                      //调整完成,break
            }       

        }
    }
    x->color = BLACK;
}

如有疑问,请指出

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值