红黑树——首身离兮心不惩

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

 

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

 

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

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);
}

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

 

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

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

 

posted @ 2018-09-05 17:04  仪式黑刃  阅读(624)  评论(2编辑  收藏  举报