上一篇讲了红黑树,只讲到了红黑树添加节点,这一篇来讲讲红黑树的删除。不得不说红黑树的删除真的是太太复杂了,我是肝了好几个星期,网上找遍了资料,也没有几个能读懂的,后来结合源码,终于是对红黑树的删除有了一些基本的理解。
先奉上一个红黑树大图,用于参照,便于后面的理解:
删除
红黑树的删除其实就是一个穷举的过程,把节点每一种的情况都考虑一遍,然后尽可能的简化操作,达到代码的精简。对待删除节点的不同状态,可以分成三个大类:
1.待删除节点没有子节点
2.待删除节点只有一个子节点
3.待删除节点有两个子节点
当待删除节点有两个节点时,用其后继节点替代待删除节点,然后转化为删除后继节点,而后继节点只有1个或0个子节点,进而转化成1类和2类
后继节点
后继节点是二叉查找树的中一个概念 ,简单来说就是比某个节点值大的所有节点中值最小的那个节点。例如最上面的那个红黑树图片中,节点6的后继节点是7,节点2的后继节点是3,依次类推
如何找到后继节点?一般来说一个节点的右子树中的最左节点就是后继节点,但也有例外一个节点的左子树中的最右节点,它的后继节点有可能是比它父节点层次还高的一个节点,例如图中节点5,它的后继节点是6
代码实现:
/**
* 返回某个节点的后继节点
* @param t
* @return
*/
private RBNode<K,V> successor(RBNode<K,V> t) {
if (t == null) {
return null;
}
// 如果t有右子节点,则后继节点就是t的右子树的最左侧节点(右子树最小的节点)
// 如果t没有右子节点,那么有两种情况:
// 1.t是其父节点的左子节点,那么他的后继节点就是父节点
// 2.t是其父节点的右子节点,那么就不断往上追溯找父节点,看是否能找到一个节点p,使得t在p的左子树中,那么首次找到的p
// 就是t的后继节点,因为此时p的键是比t键大的最小节点。如果找到根节点都找不到符合条件的p,那么说明t在根节点的右侧,
// 那么t的后继节点只能是null
if (t.right != null) {
RBNode<K, V> s = t.right;
while (s.left != null) {
s = s.left;
}
return s;
} else { // 这种场景就是上面所说的例外情况,但这种场景在红黑树删除中用不到
RBNode<K, V> p = t.parent;
RBNode<K, V> cur = t;
while (p != null && p.right == cur) {
cur = p;
p = p.parent;
}
return p;
}
}
穷举法分析删除情形
需要说明一下,由于删除有两个子节点的节点,可以转化为删除该节点的后继节点,进而转化为删除只有一个子节点或没有子节点的情况,因此后面的讨论只可能是待删除节点有0个或1个子节点
先考虑删除红色节点
由于删除红色节点不会影响任何一条路径上黑色节点的数量,因此比较简单,待删除节点又可以分下面两种情况:
1.如果没有子节点:直接删除,不影响红黑树平衡
2.如果有一个子节点,只能是黑色节点:这种情况不存在,因为当前节点是红色,有一个黑色子节点,而另一个子节点为null,该红色节点的两边黑色节点数量必不相等
再考虑删除黑色节点
也可以分为两种情况:
1.如果没有子节点(这种情况最复杂,接下来详细讨论)
2.如果有一个子节点:
2.1 如果子节点是红色:用子节点的值替换待删除节点,然后删除子节点
2.2 如果子节点是黑色:这种情况不存在,因为必然导致该节点两边黑色节点数量不一致
接下来详细讨论待删除节点是黑色,且没有子节点,这种情形分类最多,一一穷举讨论(这里先只考虑待删除节点是父节点的左子节点)
1.父节点为红色
1.1 兄弟节点(父节点的另一个子节点,下同)没有子节点,兄弟节点只能是黑色:
1.2 兄弟节点有一个子节点,且左子节点为红色:
1.3 兄弟节点有一个子节点,且右子节点为红色
情形三与情形二第二步一样
1.4 兄弟节点有两个子节点,且都为红色
2.父节点为黑色
2.1 兄弟节点为黑色
2.1.1 兄弟节点没有子节点
解决方案是兄弟节点变成红色:
这样变色会出现一个问题:待删除节点删除之后,父节点所在的路径黑色节点总数就会-1。在这种情形下,就需要让其他路径上的黑色节点数量也-1,这里我个人总结出了一个方法:向上追溯,这种方法在其他算法博文中有讲解,但不是这么命名的,且过程也不完全一致,我是根据自己的理解,想到的一个比较简单的理解方式。
**向上追溯:**就以上图来说,兄弟节点变成了红色,而待删除节点也会在后面被删除,所以父节点所在的路径黑色节点数量-1,这个时候把父节点看成待删除节点(实际不删除,只是黑色节点数-1),再带入之前的算法步骤,如果再遇到与情形5一样的情况,则继续向上追溯,直到与情形5不匹配(父节点是红色时或者父节点是根节点)
2.1.2 兄弟节点有一个子节点且左节点为红色
2.1.3 兄弟节点有一个子节点且右节点为红色
2.1.4 兄弟节点有两个子节点,都为红色(只能是红色)
2.2 兄弟节点为红色
2.2.1 兄弟节点必然有两个黑色节点
方案一:
方案一其实可能存在问题,当兄弟节点的任一子节点有一个红色子节点时,这种情况就会出现两个红色几点相连而破坏了红黑树的性质
方案二:
如上图,进行上述两步骤后,绿色圈住的部分其实又回到了一开始的情形一,由于情形一已经有解决方案,此处也顺理成章的按照情形一完成剩余工作即可。
至此,删除黑色节点的所有情况都已经穷举完毕且给出了平衡方案,如果按照穷举法,if…else的方式,完全可以把删除代码写出来,但是这样的代码不够简洁。其实我们观察上述所有情形,会发现有些情形的部分操作是一样的,并且有些情形在某个步骤之后会变成另外一种情形,这里涉及到了相同操作抽取以及情形转化,这是简化的关键。
删除简化
情形转化
对以上所有情形进行细致观察,会发现有些情形在某个步骤之后会变成另外一种情形。
1.情形九在两个步骤走完后会转成情形一~情形三中的任一情况
2.情形二在第一步旋转变色后与情形三一样
3.情形六在第二步之后与情形七一样
相同操作抽取
对以上所有情形进行细致观察,会发现有些情形的部分操作是一样的,可以用一套代码来套用。
1.情形二和情形三的最后一步均给出了两个方案,但是结合情形四,会发现情形四就是使用了情形二、三的第二套先变色再旋转的方案,于是我们不在使用第一种方案,上述三种情形的均使用先变色再旋转的方案
2.情形七和情形八操作一致
经过上述的简化和抽取,需要考虑的分类还是太多,其实如果研究JDK红黑树源码,会发现还能进一步抽取相同操作:
情形一与情形五都可以使用向上追溯法,由于向上追溯的终止条件是待操作节点的父节点是根节点或者父节点为红色。情形一由于待删除节点的父节点是红色,在一轮追溯后就停止,而情形五可能更多次
情形二、三、四都有如下的操作如下:
情形六、七、八都有如下的操作:
上述两类操作都是先变色后旋转,如果变色过程能用一套逻辑,那么将极大的简化代码。第一类变色过程:兄弟节点变红,紧接着父节点和兄弟节点的右子节点变黑,第二类变色过程:兄弟节点维持黑,父节点维持黑,兄弟节点的右子节点变黑。
归纳:先将兄弟节点变成和父节点一样的颜色,然后父节点和兄弟节点的右子节点变成黑色
经过上面的简化操作,我们就可以开始写代码了。
代码
综合上述分析,我们可以将删除节点的过程分为删除节点和节点修正两个过程
1.如果待删除节点有两个子节点,需要找到其后继节点,用后继节点的值代替待删除节点,然后转化为删除后继节点
2.如果待删除节点有一个子节点,先删除节点,然后对剩下的节点进行修正
3.如果待删除节点没有子节点,通过对上面的过程分析,可以发现先删除节点再修正与先修正再删除节点结果一样,所以完全可以先对节点进行修正,最后再删除节点
完整删除代码:
/**
* 删除节点,并返回删除的节点
* @param key
* @return
*/
public V remove(K key) {
RBNode<K, V> p = getRBNode(key);// 根据key找到待删除节点
if (p == null) {
return null;
}
V oldValue = p.value;
deleteRBNode(p);
return oldValue;
}
/**
* 删除指定节点
* 删除:
* 1.待删除节点没有子节点
* 2.待删除节点只有一个子节点
* 3.待删除节点有两个子节点
* 当待删除节点有两个子节点时,用其后继节点的值替代待删除节点,然后转化为删除后继节点,后继节点只有一个或者没有子节点
* @param p
*/
private void deleteRBNode(RBNode<K,V> p) {
// 如果待删除结点有两个子节点,需要找到其后继节点,然后转化为删除后继节点
if (p.left != null && p.right != null) {
// 1.找到后继节点
RBNode<K, V> s = successor(p);
// 2.用后继节点的属性替换待删除节点(键和值)
p.key = s.key;
p.value = s.value;
// 3.p指向后继节点,接下来就是对后继节点进行删除
p = s;
}
// 如果待删除节点有一个子节点,可以用待删除节点的子节点替换待删除节点的位置,再对剩下的节点做修正
RBNode<K, V> r = (p.left != null ? p.left : p.right);// r是p的子节点,优先找左子节点,如果没有则取右子节点
if (r != null) { // 有一个子节点
r.parent = p.parent;
if (p.parent == null) {
r = root;
} else if (p.parent.left == p) {
p.parent.left = r;
} else {
p.parent.right = r;
}
p.left = p.right = p.parent = null;
if (p.color == BLACK) { // 如果p为黑,删除p后路径上黑色-1,则需要进行修正
fixAfterDeletion(r);
}
} else if (p.parent == null) {
root = null;
} else { // 没有子节点
if (p.color == BLACK) { // 待删除节点为黑色,且没有子节点,先对节点进行修正
fixAfterDeletion(p);
}
// 最后删除p
if (p.parent != null) {
if (p.parent.left == p) {
p.parent.left = null;
} else {
p.parent.right = null;
}
p.parent = null;
}
}
}
/**
* 修正红黑树(删除)
* 针对的节点没有子节点,且需要调整颜色或者旋转的情形
* 在修正时,由于待操作节点并没有真正删除,所以可以看成是黑色值-1
* @param x
*/
private void fixAfterDeletion(RBNode<K,V> x) {
while (x != root && colorOf(x) == BLACK) { // “向上追溯”
if (x == leftOf(parentOf(x))) { // 需要修正的节点是父节点的左子节点
RBNode<K,V> xr = rightOf(parentOf(x));// 兄弟节点
if (colorOf(xr) == RED) { // 情形九,转化成情形二三四
setColor(xr, BLACK);
setColor(parentOf(x), RED);
rotateLeft(parentOf(x));
xr = rightOf(parentOf(x));
}
// 情形一在父节点变红后,就退出循环,然后将父节点涂黑;情形五,需要向上追溯,直到不再是情形五
if (colorOf(leftOf(xr)) == BLACK
&& colorOf(rightOf(xr)) == BLACK) {
setColor(xr, RED);
x = parentOf(x);
} else { // 无须向上追溯
if (colorOf(rightOf(xr)) == BLACK) { // 情形二转化为情形三、情形六转化为情形七
setColor(xr, RED);
setColor(leftOf(xr), BLACK);
rotateRight(xr);
xr = rightOf(parentOf(x));
}
// 转化后的共性操作
setColor(xr, colorOf(parentOf(x)));
setColor(rightOf(xr), BLACK);
setColor(parentOf(x), BLACK);
rotateLeft(parentOf(x));
x = root; // 跳出循环
}
} else { // 对称
RBNode<K,V> xl = x.parent.left;
if (colorOf(xl) == RED) {
setColor(xl, BLACK);
setColor(parentOf(x), RED);
rotateRight(parentOf(x));
xl = leftOf(parentOf(x));
}
if (colorOf(rightOf(xl)) == BLACK &&
colorOf(leftOf(xl)) == BLACK) {
setColor(xl, RED);
x = parentOf(x);
} else {
if (colorOf(leftOf(xl)) == BLACK) {
setColor(rightOf(xl), BLACK);
setColor(xl, RED);
rotateLeft(xl);
xl = leftOf(parentOf(x));
}
setColor(xl, colorOf(parentOf(x)));
setColor(parentOf(x), BLACK);
setColor(leftOf(xl), BLACK);
rotateRight(parentOf(x));
x = root;
}
}
}
setColor(x, BLACK);
}
红黑树的原理的确比较复杂,如果不借助笔记和画图工具,很难理解。因此此文以及上一篇文章用来记录自己学习红黑树的心得,如果有疑问或者有误之处,欢迎评论探讨。