红黑树03——节点的插入与删除-步骤与示例.md

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新的左孩子从而造成连续两个红色。
分类 描述 处理方式 后续说明
1 pb为红色 p置黑,pp置红,pb置黑,x指向pp,进行下一轮循环判断。 pp由黑置红,可能会违反不能有连续两个红结点的规则,故循环继续。
2 pb为黑色,x、p、pp不在一条直线上 p左旋,重置x和p,使之在一条直线上 转化为情况3。
3 pb为黑色,x、p、pp不在一条直线上 p置黑,pp置红,pp右旋 已经平衡,结束。
    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种情况分别是什么时,不妨自己在纸上画画,强迫自己搞点事情出来,再逐步恢复之前的局势,想必能帮助你想起来。

分类 描述 处理方式 后续说明
1 b为红 b置黑,p置红,左旋p,重置b,继续 转化为情况2/3/4的一种。注意,这里的重置b别忘了。
2 b, bl, br均为黑 b置红,r指向p,继续 2和3、4是排他的,而3和4在同一次循环操作中
3 b为黑,bl为红 b置红,bl置黑,右旋b,再重置b 转化为情况4。注意,重置b别忘了。
4 b为黑,br为红 br置黑,p, b互换颜色,左旋p,r指向root 结束
    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种情况。话说你这两串数字怎么得来了,你怎么能知道能出现所有的情况?嘿嘿,多运行几次代码,你就能挑到一个满足条件的。

开始插入5

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

完成插入5

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

插入4

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

插入2和1

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


插入0

现在插入完毕,哈哈,看起来左边高很多,但也只是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。

删除6

继续删除结点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.

删除5

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

删除1

接下来删除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

转载声明

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,529评论 5 475
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,015评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,409评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,385评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,387评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,466评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,880评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,528评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,727评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,528评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,602评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,302评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,873评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,890评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,132评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,777评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,310评论 2 342

推荐阅读更多精彩内容

  • 写在前面 当在10亿数据进行不到30次比较就能查找到目标时,不禁感叹编程之魅力!人类之伟大呀! —— 学红黑树有感...
    安卓大叔阅读 658,445评论 262 1,256
  • B树的定义 一棵m阶的B树满足下列条件: 树中每个结点至多有m个孩子。 除根结点和叶子结点外,其它每个结点至少有m...
    文档随手记阅读 13,144评论 0 25
  • 1、属性传值,OC中中最简单的传值方式。它的使用方法,只需要在推出下一个页面之前,将下一个页面中接受属性在本页面进...
    SecTwilight阅读 1,659评论 2 2
  • 1份宁静阅读 185评论 0 0
  • 心的湖面起雾 曾涌动着的波涛泛冰 站在炙热的太阳下也不会感受到半点温暖 全身麻麻的 手脚抽搐 想握紧拳头 却一点儿...
    翱蓝阅读 157评论 0 0