zoukankan      html  css  js  c++  java
  • 红黑树——首身离兮心不惩

    最后我们来探究红黑树的删除算法,相比插入操作,它的情况更复杂一些。因此直接考虑很容易撞到南墙,我们更需要利用转化与化归的思想(还记得高中数学四大思想方法吧,这里一样适用),通过提升变化,把红黑树映射成一颗B-树,并站在后者的角度,反过来理解前者的原理。但我们更需要关心重构操作,也就是一系列的旋转和修复过程,并在此过程中留意他的重构次数都是$Oleft( 1 ight)$级别的

    这个删除操作还挺复杂的。。。可以说繁琐到了让人恶心的地步,各位做好心理准备。可以在旁边准备个塑料袋或者盆什么的,省的没地方。。。

     

    先给出一些辅助函数,方便后续使用

    Position p=NULL; //用于存储父节点的地址
    
    Position SearchIn(Position v,int e,Position& parent){//返回指向父节点指针的引用,因为后续要做左值
        if (!v || (e == v->value)) return v; //递归基,如果直接命中或不存在则返回
        parent=v; //一般情况则是先记下当前节点,然后深入一层。
        return SearchIn((e < v->value ? v->left : v->right), e, parent);
    }//返回值指向命中节点,parent为其父。
    
    Position Search(int target,RedBlackTree T){
        return SearchIn(T, target, p);
    }
    
    Position GetParentOf(int e){
        Search(e, fir);
        return p;
    }
    
    //由于我写的结构体里没有parent指针,所以通过这个函数等效获取。
    Position GetParentOf(Position T){ //得到T的父节点
        Search(T->value, fir);
        return p;
    }
    
    int IsLChild(Position p){  //判断p点是否为某个节点的左孩子
        if (GetParentOf(p) != fir  && GetParentOf(p)->left == p)
            return 1;
        else return 0;
    }
    
    Position& FromParentTo(Position p){  //返回某个节点来自父亲的指针
        if(GetParentOf(p) == fir) return p; //处理P是根的情况
        if (GetParentOf(p) ->left ==p)   // p is leftChild
            return GetParentOf(p)->left;
        else
            return GetParentOf(p)->right;
    }

    红黑树的删除也类似BST删除,自顶向下(参看前面二叉树实现一文),如果被删除节点X并非叶子,我们会考虑用它的直接后继(右子树上的最小元素)代替,这样X顶多有一个右孩子,或者X被转移到叶子的位置,直接删除即可。但有一些情况需要仔细分析,因为红黑树规则有其他的限制。

    比如在这副图中节点x在被删除之后,将由它的某一个后代r来替代,这样一来红黑树的性质就未必都能继续满足了,验证一下:首先红黑树的根、外部节点没有受影响,但在此局部有可能会出现两个连续的红色节点,而更重要的是在被删除节点所在的路径上,黑节点的数目 可能变化,第四条规则不一定能满足了。另外有一大类的情况,还是非常容易处理的。就是被删除节点x 与它的替代者r之间有一个是红的(当然不可能全红),如上下这两张图的情况。

    这种情况只要把替代者r染为黑色即可,就保证第4条规则不受影响了。原因在于,从删除操作之前的树结构可见,在此局部都包含一条指向红节点的虚边,上篇说过这类虚边对于黑高度是没有影响的,因此在把r染黑之后,都相当于删除了一条虚边,因此所有外部节点的黑深度不受影响红色叶子的删除还好说,问题就在于如果这叶子是黑色的,删除之后规则4就被破坏了。解决思路就是:保证从上到下删除期间树叶始终是红色的。下面详细分析一下这一类情况。

    有可能被删除节点和替代者都是黑色的,

    这种情况我们也称之为双黑,此时这两个节点所属的那条路径而言黑长度必然会减少一个单位,从而必然违背红黑树的第四条规则。而且不幸的是,前面简明的方法,也不再有效。在给出新的方法之前,我们或需要从另一个角度来体会,问题究竟出在哪。什么角度呢 ?当然啦,就是B树。如果x和r都是黑色的,那么在对应的4阶B树中,x将独自成为一个内部节点

    于是在唯一的这个关键码被删除之后,这个节点也就发生下溢。因此我们的调整算法与其说是在红黑树中修复双黑缺陷,不如说是在B树中修复下溢缺陷。为此我们需要考察两个节点:首先是删除之后,节点r的父亲p;此外我们还需要在原树中考察节点r的兄弟S。

    先给出在某处删除节点的办法,和BST很像

    Position removeAt(Position x){
        Position temp;
        if (x->left && x->right) {
            temp=FindMin(x->right);
            x->value=temp->value;
            x->right=removeAt(x->right);
        }
        else{
            temp=x;
            if(!x->left) x=x->right;
            else if (!x->right) x=x->left;
            free(temp);
        }
        return x;
    }//返回被删除节点的位置

    以下我们就分4种情况分别处置

    第一种情况:S为黑,且至少有一个红色孩子

    以一字型为例(左),其余的情况都与之对称或相似。调整办法就是做相应旋转和重新染色(右)。染色规则是:r继续保持黑色,而t和p都染黑,而s将继承此前根节点p的颜色。

    这里的4棵子树其黑高度都是一样的,因此调整之后红黑树的所有性质都恢复了。这一转换方法并非偶然,而是有着深刻的原理,就是B树。接下来就让我们转到B树的角度,来反观这种变换的效果。

    可以看到,双黑缺陷对应于一次下溢,所幸的是,发生下溢的这个节点拥有一个足够富有的兄弟,可以通过旋转消除下溢。具体来说下溢节点将从父亲那借得一个关键码,而父亲再向那个兄弟借入一个关键码以填补空缺。

    经过这样的旋转,可以看到下溢节点得到了修复。

    接下来把修复之后的B树,还原为对应的那棵红黑树即可,与直接在红黑树上所做的变换是完全一致的。

    新的这个关键码 会依然继承它前任的颜色,所以绝对不会在其他位置再次造成双黑。从这个意义上讲,这种情况是相对简单的,体现在可以仅通过一次旋转完成。换句话说至少有一个红色孩子。那这种情况既然是简单的,我们也很容易得知:更难的情况是没有一个孩子为红。那又该如何应对呢?

    第二种情况:S为黑,两个孩子都为黑。但是P是红色。

    这种情况又进而分为两种子情况,它们的区别就在于:此时的父节点P究竟是红还是黑,我们先讨论红色的情况。

    首先将此前的红黑树 转换为对应的B树,依然在这个位置上发生了一次下溢。此时我们并不能实施旋转调整,原因是此时的兄弟节点s已经没有余粮了,自己已经处于下溢的边缘试探了,并不足以借出任何的关键码。还记得之前怎么处理的吧,合并。从父节点中取出一个元素,并且以它作为粘合剂,将左和右两个节点合二为一。修复的结果如下:

    然后变换回对应的红黑树,就可以得到在红黑树中的一种可行调整方案: 

    现在站在红黑树的角度来观察这个过程,结果相当于r保持此前的黑色,而s由黑转红,同时p由红转黑。所以在红黑树中的上述调整过程,完全等效于在B树中某个节点通过与它的兄弟合并来消除下溢。而且这一个局部双黑缺陷的修复,也意味着红黑树的性质能够得以在全局得到恢复,一次修复,彻底修复。

    第三种情况:S为黑,两个孩子都为黑。而且P是黑色。

    同样的站在B树的角度来看,此时依然会发生一次下溢,而且同样只能通过兄弟节点的合并来加以消除。

    与第二种情况的不同之处在于,此时的元素p是独自成为一个内部节点,因此当这个唯一的元素p被借出之后,此前的父节点将注定发生下溢。也就是说,在这种情况下双黑缺陷有可能会向上传播一层,甚至继续上传,直到最后的树根。如果还采用老办法修复,至多也就发生logn次,那问题来了,拓扑结构也会随之变化logn次?这可不是个好消息。

    其实只要回到红黑树,就可以形象地理解这个复杂的调整

    需要再次强调的是:经过这样的调整,红黑树的拓扑结构没有实质变化。也就是说 整个调整过程所执行的重构操作,不超过O(1)依然有可能落实。以下 我们只剩下最后一种情况。也就是兄弟节点s有可能不是黑色,而是红色。

    第四种情况:S为红,孩子均为黑

    参照普通BST的删除,我们只需要转化为之前的某种情况就行了,而不用另起炉灶。为此我们需要再次站在对应B树的角度:

    此时的p和s共同的结为一个3分支的内部节点,在此时的B树中,只需令s和p互换颜色,而无需做任何实质的结构调整。当然在对应的红黑树中,需要做一次结构调整。具体来说就是要围绕节点p旋转,同时翻转s和p的颜色。

    到这里我们或许有些失望,因为问题并没有解决。比如原先黑高度的异常依然存在。然而实际上这步转换并非没有意义,因为此前的矛盾焦点在于节点r的兄弟s为红色,现在在无形中r已经拥有了一个黑的兄弟s',于是此后必然会跳出第四种情况,而转入此前所讨论的3种情况。而更好的消息是,下面只可能转入其中的第1或者第2种情况,而不会是第3种。因为第3种的特征是父节点p必须是黑的,经过刚才的变换,p已经悄然变成红色。而12的情况的计算复杂度更小,因为不会向上蔓延。所以经过如此调整之后,只需再做一轮递归,整个红黑树必然会完整修复。

    那么具体的代码实现就是:

    void solveDoubleBlack(Position x){  //双黑缺陷的修复
        Position p=GetParentOf(x);  //r的父亲
        if(!p) return;
        Position sibling= (x==p->left)? p->right : p->left;//r的兄弟
        if (sibling -> col==black){  //兄弟为黑
            Position temp=NULL;
            if( sibling ->left && sibling->left->col==red)
                temp=sibling->left;
            else if (sibling ->right && sibling->right->col==red)
                temp=sibling->right;
            if (temp) {  //情况1:黑s有红色孩子
                Color oldCol=p->col;//备份原来的根p的颜色,
                FromParentTo(p) =Rotate(sibling->value,p);//做重平衡
                Position b=FromParentTo(p);
                //然后把新子树的左右孩子染黑
                if(b->left) b->left->col=black;
                if(b->right) b->right->col=black;
                b->col=oldCol;    //新树根继承原来的颜色
            }
            else{   //情况2、3:黑s无红色孩子
                sibling->col=red; //s转红
                sibling->Height--;
                if(p->col ==red)    //情况2
                    p->col=black;
                else{            //情况3
                    p->Height--;  //颜色保持,但是黑高度减1
                    solveDoubleBlack(p);
                }}}
        else{ //情况4:兄弟为红
            sibling->col=black;
            p->col=red;  //s转黑,p转红
            Position t= IsLChild(sibling)?sibling->left:sibling->right;
            FromParentTo(p)=Rotate(sibling->value,p);
        }
        solveDoubleBlack(x);
    }
    
    
    
    //正式的删除过程
    void Delete(int e,RedBlackTree T) {
        Position X = Search(e, T);  //X指向被删除节点
        if(!X)  printf("%d not found!",e);
        Position r=removeAt(X);
        if(GetParentOf(r) ==fir)//如果是根节点,将其染黑
            r->col=black;
        
        if (r->col==red) //如果r为红色,直接染黑就行
            r->col=black;
        //以下情况:原来的x(现在的r,因为被删除了嘛,然后替换了)均为黑色
        solveDoubleBlack(r);
    }

    看起来就很。。。一言难尽。这个代码暂时还有一些小问题,但是足够帮助我们理解删除过程了,可以暂且当成伪代码。 本来想着先不放上来吧,但是光看图理解的话,似是而非,所以还是看看代码实现吧。

    现在作一总结,以下是删除的情况分析:

    每一次删除操作 在每一高度上至多只会花费常数时间,由此可知红黑树的删除时间复杂度不会超过$Oleft( log n ight)$。通过以上概括可以发现,红黑树的删除至多只需做$log n$次的重染色,以及常数次的结构调整。这也是红黑树优于AVL树的一个重要方面。还记得吧,在介绍红黑树伊始就提到过,这个特性对于持久性结构的实现是至关重要的。

  • 相关阅读:
    mysql复习相关
    OpenStack三种类型的NAT转换
    openstack资料相关
    [转]Web 调试工具之 Advanced REST client
    [转]Aspose.Words.dll 将 Word 转换成 html
    [Android] 开发第十天
    [win10]遇坑指南
    [转]Explorer.exe的命令行参数
    [Android] 开发第九天
    [Android] 开发第八天
  • 原文地址:https://www.cnblogs.com/hongshijie/p/9593196.html
Copyright © 2011-2022 走看看