每个叶子节点(nil)是黑色。_红黑树03——节点的插入与删除-步骤与示例

0. 前言

我们采用nil代替null来简化操作。如果你之前学过,有一些印象,那跟随本文从上到下画一画插入与删除的全过程,也能加深你的印象与熟练程度。

插入和删除的主逻辑,与前面的文章——红黑树01——前传-二叉搜索树中基本一致,只是特别处理了一下nil。最大的不同是,当插入结点和它的父结点都为红色时,违反了红黑树的属性,需要重新调整,共有3种情况(应该是6种,因为是对称的,所以只讨论一半);删除结点时若涉及到黑结点的删除(或替换),会导致一侧黑结点数目减少,违反了红黑树的规则,所以也需要调整,共有4种情况。

本章内容比较长,如果看得吃力了,可以暂时停下休息一会,回头再看。但长不可怕,可怕的是看不懂,所以本文中我尽可能地把各个操作的前因后果讲清楚,不像很多书上直接告诉你最终怎么做,但不知道原理所以一直记不住。希望大家能耐心看下去,经过测试,我给部分同学讲过之后,能轻松记住整个过程,且能写出来全部代码。一定要坚持看完弄懂,不要觉得麻烦就不看了,请相信我,欠下的债迟早要还的。

1. 插入后的调整

为了便于说明,设新插入结点为x,x的父结点为p,p的父结点为pp,p的兄弟结点为pb,pb的左孩子为pbl, pb的右孩子为pbr。因为对称性,我们这里只讨论p == pp.left的情况。已经x和p当前均为红色,pp肯定为黑色。这一系列的字母,直接这样看下去肯定会晕的,所以请画出来,自己边画边往下读。也可以直接根据结论看文章后面的示例,再回头来看这里讨论的原因。

回到主题:因为有两个红色结点,我们需要把其中一个变成黑色。但置黑的肯定不是x,因为新插入的结点是红色(为什么新插入的是红色呢,因为如果不插入红色的话,那就没有红色了。但为什么不担心没有黑色了呢?因为强制规定——根为黑色,所以能保证一定有黑色),所以我们将p置黑。

但这样pp的左子树就多了一个黑色,违反规则,现在要想办法减少左边的黑色,前面已分析了,x本身不能变,故只能使pp置红。但又导致右边少一个黑色,所以需要把右边的黑色加上来,有两种办法: 1. 若pb为红色,将pb置黑即可。因为pp置红了,可能会违反不能连续两个红结点的规则,所以将x指向pp,进入下一轮循环继续; 2. 若pb为黑色,只能右旋pp(还记得我们上一篇文章中旋转的作用吗?)。但要想pp能够成功右旋,x、p、pp必须在一条直线上,否则x将作为pp新的左孩子从而造成连续两个红色。

04f268ec64f27542ffc128d7f8b2211f.png
private void insertFixup(Node x) {
        while (x.parent != nil && x.parent.color == RED) {
            Node p = x.parent;
            // 因为父节点为红色,而根节点为黑色,所以父节点肯定不为根,故祖父节点存在,所以这里不需要判断为nil。
            Node pp = p.parent;
            pp.color = RED;
            if (pp.left == p) {
                Node pb = pp.right;
                if (pb.color == RED) {
                    insertFixUpStats[0]++;
                    p.color = BLACK;
                    pb.color = BLACK;
                    x = pp;
                } else {
                    if (p.left != x) {
                        insertFixUpStats[1]++;
                        rotateLeft(p);
                        x = p;
                        p = x.parent;
                    }
                    insertFixUpStats[2]++;
                    p.color = BLACK;
                    rotateRight(pp);
                }
            } else {
                Node pb = pp.left;
                if (pb.color == RED) {
                    insertFixUpStats[0]++;
                    p.color = BLACK;
                    pb.color = BLACK;
                    x = pp;
                } else {
                    if (p.right != x) {
                        insertFixUpStats[1]++;
                        rotateRight(p);
                        x = p;
                        p = x.parent;
                    }
                    insertFixUpStats[2]++;
                    p.color = BLACK;
                    rotateLeft(pp);
                }
            }
        }
        root.color = BLACK;
    }

2. 删除

当黑色结点被替代者替换时,会导致黑色结点的缺失而违反性质4,我们需要对替代者做一些处理,使树恢复平衡。设被删除的结点为x,x的左孩子为xl(left),右孩子为xr(right),x的后继为s(successor),后继的右孩子为sr,替代者为r(replacement),被替代的颜色为rc(replacedColor),x的父结点为p,x的兄弟节点为b,b的左右孩子分别为bl, br。

因为删除调整也是对称的,下面我们只讨论x == p.left的情况。

2.1 确定替代者与被替代的颜色

  1. 如果xl或xr不存在时,直接把xr或xl替换x即可,故r为xr或xl,rc为x.color;否则转2。
  2. 如果xr == s,则只发生一次替换:s -> x,故r为s,rc为x.color。
  3. 如果xr != s,则发生两次替换:sr -> s, s -> x。我们保留x位置的颜色不变,将x的颜色赋值给s,则在颜色上的替换只有一次:sr -> s,我们只需要调整一个结点的黑色结点问题,此时r为s.right,rc为s.color。

2.2 调整替代者

因为少了一个黑色的结点,所以,以根到x.parent整条路径上的任意结点为根的子树,其左子树都会少一个黑色结点。前人发明了一招来治标:假设r指针附带了一层额外的黑色(这个黑色是跟着r指针走的,与结点本身无关,当r向上移动的时候,这层黑色也会跟着指针r走),这样少的那个黑色又加回来了。如果r节点为红色,那将r置黑(即将虚拟的黑色实体化),即解决问题;否则,需要更多的调整。

先确定总的原则:1. x不能再变化,因为x的变化会导致左边黑色进一步减少,无利于解决问题;2. 总需要做出点变化,不然静止不动,肯定无法解决问题。

首先区分出4种情况:按b的颜色可分为两种情况,而b为黑时,又根据bl, br的颜色,可分为全黑、左红、右红3种情况(为什么不考虑全红:因为单独考虑左红、右红已经包含全红了。另外,右红经过左旋可转化为左红,是的,跟上面插入时一样,如果不在一条直线上则旋转到一条直线上)。然后再逐个攻破:

  • 情况1:如果b为红色,则p、bl和br都为黑色。因不能连续两红,故p、bl、br都不能变色,唯一能做的只能是b,故将其置为黑色,这样右边又多了一个黑色,现在多了2个黑色,如果现在左旋最多只能减少一个,那就先把p置红(这里为什么敢置红呢?先试试嘛,万一不行不变就是了,恰好接下来的左旋操作,使得p的父亲变成黑色的b,所以不会出现两个红色),右边黑色数目恢复,但左边又少了一个黑色。那就p左旋(旋转就是用来将一边的颜色分享给另一边而原来那一边颜色不减少),这时b的黑色作用到两边使左右两边的黑色数目都恢复。另外p左旋导致原来的bl成了p.right,成为x的新兄弟,且bl是黑色的,故转换为情况2/3/4的一种。
  • 情况2:b, bl, br都为黑色,唯一可能的变化是右边的某个结点由黑转红,如果该结点是bl或br的话,那会造成b子树的不平衡,不但没解决问题反而引入了新的问题,肯定不行,所以只能是b由黑转红。这时右边就少了一个黑色。将r上移指向p,相当于将r的虚拟黑色分享给右边,增加了一个黑色。但此时只解决了原r子树的黑色不平衡问题,新的r及更上层的不平衡问题还未解决,故进入下一轮循环继续。
  • 情况3:b为黑色,bl为红色,因p, b, bl不在一条直线上,参考插入过程,我们需要将p, b, bl调整到一条直线上来以便进行旋转操作。故b置红,bl置黑,右旋b,再重置b,转为情况4。
  • 情况4:b为黑色,br为红色,则p, b, br在一条直线上,这个时候怎么搞事情呢?因为br是红色,所以b不能再置红,而bl和p颜色不确定,所以也不能直接修改颜色。故可能的变化只能是br置黑,则右边多了一个黑色。因为不能将p置红来减少右边的黑色,唯一能做的就是想办法把b的黑色弄到左边去。首先想到的就是左旋p,但如果p是黑色的,左旋p会导致右边黑色减1左边加1,最终左边黑色数目比右边多2(算上那个虚拟黑色),所以不能直接左旋p。我们试着交换p和b的颜色,再左旋p,则p原来的颜色还在原来的位置,b的黑色成功置换到了左边。但现在左边又多了一个黑色,我们把r移走,指向root,则总体平衡了(右边黑色一加一减不变,左边增加了一个),结束处理。

所以将来大家记不住4种情况分别是什么时,不妨自己在纸上画画,强迫自己搞点事情出来,再逐步恢复之前的局势,想必能帮助你想起来。

d28bee4eb444c11185e59c102a53b5ec.png
private void deleteFixUp(Node r) {
        while (r.color == BLACK && r != root) {
            Node p = r.parent;
            if (r == p.left) {
                Node b = p.right;
                // 情况1,处理之后转化为情况2/3/4的一种。
                if (b.color == RED) {
                    deleteFixUpStats[0]++;
                    b.color = BLACK;
                    p.color = RED;
                    rotateLeft(p);
                    b = p.right;
                }

                // 因为有哨兵nil,所以这里不需要判断brother.left是否存在。
                if (b.left.color == BLACK && b.right.color == BLACK) {
                    deleteFixUpStats[1]++;
                    b.color = RED;
                    r = p;
                } else {
                    // 情况3转化为情况4
                    if (b.left.color == RED) {
                        deleteFixUpStats[2]++;
                        b.left.color = BLACK;
                        b.color = RED;
                        rotateRight(b);
                        b = p.right;
                    }
                    deleteFixUpStats[3]++;
                    b.right.color = BLACK;
                    b.color = p.color;
                    p.color = BLACK;
                    rotateLeft(p);
                    r = root;
                }
            } else {
                Node b = p.left;
                if (b.color == RED) {
                    deleteFixUpStats[0]++;
                    b.color = BLACK;
                    p.color = RED;
                    rotateRight(p);
                    b = p.left;
                }

                if (b.left.color == BLACK && b.right.color == BLACK) {
                    deleteFixUpStats[1]++;
                    b.color = RED;
                    r = p;
                } else {
                    if (b.right.color == RED) {
                        deleteFixUpStats[2]++;
                        b.right.color = BLACK;
                        b.color = RED;
                        rotateLeft(b);
                        b = p.left;
                    }
                    deleteFixUpStats[3]++;
                    b.left.color = BLACK;
                    b.color = p.color;
                    p.color = BLACK;
                    rotateRight(p);
                    r = root;
                }
            }
        }
        r.color = BLACK;
    }

3. 示例

下面我将演示按{6, 3, 5, 4, 2, 1, 0}的顺序逐个插入,再按{6, 4, 5, 0, 1, 3, 2}的顺序逐个删除结点,演示插入的3种情况和删除的4种情况。话说你这两串数字怎么得来了,你怎么能知道能出现所有的情况?嘿嘿,多运行几次代码,你就能挑到一个满足条件的。

aa4cc0ecd28b21fdb6c63a4105e06a38.png

前面两个结点太简单,就不演示了。我们从插入5开始,现在3和5连续两个红结点,且6、3、5不在一条直接线,为插入情况2,我们左旋3,转化为情况3;然后5置黑,6置红,右旋6.

385026b460864f8dbaf7d165cef95830.png

接下来插入4之后,3和6都为红,为情况1,所以将3和6置黑,5置红。然后继续判断的时候,x已经指向根,所以将根置黑即结束。

c15292f9191cfe9e27c7e7826cba5d2c.png

接下来插入2,不需要调整,所以不演示了,继续插入1.此时2和4均为红,为情况1,将2和4置黑,3置红,因为3的父结点5不再是红色,所以处理结束。

2ef044382ace64663eedb655b5b2665a.png

插入0,0、1、2在一条直线上,为情况3,所以1置黑,2置红,右旋2.

c93f17bda53e72effad34856c34c7af0.png

现在插入完毕,哈哈,看起来左边高很多,但也只是2倍啦,还是一棵合法的红黑树。接下来我们开始删除操作{6, 4, 5, 0, 1, 3, 2}。继续使用上文中的符号来表示。

删除6。因6的左孩子为nil,所以r为其右孩子(也是nil,但没关系,我们把nil当成普通结点看待),rc为6的颜色黑色,所以需要做调整。b为3,颜色为红,符合情况1。所以将3置黑色,5置红色,右旋5,再重置b。

此时b(4)为黑色,左右孩子均为nil,也为黑色,符合情况2,故4置红,r指向5。

此时r本身为红色了,所以结束循环,将其置黑,结束处理。总结一下,删除结点6,我们共进行了2次调整,分别为情况1和情况2。

3f4ebe61c91e92be7d7976d200c78fc4.png

继续删除结点4。因为4左孩子不存在,所以r为其右孩子,rc为4的颜色红色,故不需要调整,这里不再单独作图。

再删除5。因5的左孩子不存在,故r为其右孩子nil,rc为5的颜色黑色,所以需要调整。因为b(1)为黑色,且其右孩子为红色,符合情况3,故1置红,2置黑,b左旋,2成为新的b,转情况4;情况4中,将bl(1)置黑,p(3)颜色给b,p置黑,右旋3,r指向root。

总结一下,删除结点5,我们共进行了2次调整,情况3和情况4.

f40919f1eb41bda49847bb7facf4fc2f.png

接下来删除0,因它没有左右结点,且颜色为红色,不需要调整,直接删除即可,不再画图。接下来删除1。1没有左结点,故r为1,rc为1的颜色黑色,b为3,颜色为黑色,且3的左右孩子均为黑色(nil的颜色),符合情况2,故将b置红,r指向p。因为此时p已经是根了,将其置黑结束处理。

75b02f97ff6c72c425b56ec54b1c115d.png

接下来删除3,3为红色,不需要调整,删除2树为空,也不需要调整,不再画图。

5. 结语

通过本文的练习,如果能正确得到结果,证明你已经基本掌握了红黑树的插入与删除操作。记得,插入调整的3种情况和删除调整的4种情况,都可以通过自己演练出来,记不住了,就在纸上画画,试着随便去做一些调整(如果调整造成了其他的冲突,则想办法往回调整),最终会得到正确的解决办法。

源码

https://github.com/readyou/algorithm-introduction-code/blob/master/src/main/java/me/wxl/demo/data/struct/RedBlackTree.java

作者微信

1806ef92408ab2a95a156dbd83d2754f.png

转载声明

如果您需要转载,请注明转载来源。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值