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置红。但又导致右边少一个黑色,所以需要把右边的黑色加上来,有两种办法:
- 若pb为红色,将pb置黑即可。因为pp置红了,可能会违反不能连续两个红结点的规则,所以将x指向pp,进入下一轮循环继续;
- 若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 确定替代者与被替代的颜色
- 如果xl或xr不存在时,直接把xr或xl替换x即可,故r为xr或xl,rc为x.color;否则转2。
- 如果
xr == s
,则只发生一次替换:s -> x,故r为s,rc为x.color。 - 如果
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开始,现在3和5连续两个红结点,且6、3、5不在一条直接线,为插入情况2,我们左旋3,转化为情况3;然后5置黑,6置红,右旋6.
接下来插入4之后,3和6都为红,为情况1,所以将3和6置黑,5置红。然后继续判断的时候,x已经指向根,所以将根置黑即结束。
接下来插入2,不需要调整,所以不演示了,继续插入1.此时2和4均为红,为情况1,将2和4置黑,3置红,因为3的父结点5不再是红色,所以处理结束。
插入0,0、1、2在一条直线上,为情况3,所以1置黑,2置红,右旋2.
现在插入完毕,哈哈,看起来左边高很多,但也只是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。
继续删除结点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.
接下来删除0,因它没有左右结点,且颜色为红色,不需要调整,直接删除即可,不再画图。接下来删除1。1没有左结点,故r为1,rc为1的颜色黑色,b为3,颜色为黑色,且3的左右孩子均为黑色(nil的颜色),符合情况2,故将b置红,r指向p。因为此时p已经是根了,将其置黑结束处理。
接下来删除3,3为红色,不需要调整,删除2树为空,也不需要调整,不再画图。
5. 结语
通过本文的练习,如果能正确得到结果,证明你已经基本掌握了红黑树的插入与删除操作。记得,插入调整的3种情况和删除调整的4种情况,都可以通过自己演练出来,记不住了,就在纸上画画,试着随便去做一些调整(如果调整造成了其他的冲突,则想办法往回调整),最终会得到正确的解决办法。
源码
转载声明
如果您需要转载,请注明转载来源。