四 红黑树的插入
从红黑树上删除一个节点,可以先用普通二叉搜索树的方法,将节点从红黑树上删除掉,然后再将被破坏的红黑性质进行恢复。
我们回忆一下普通二叉树的节点删除方法:Z指向需要删除的节点,Y指向实质结构上被删除的结点,如果Z节点只有一个子节点或没有子节点,那么Y就是指向Z指向的节点。如果Z节点有两个子节点,那么Y指向Z节点的后继节点(其实前趋也是一样的),而Z的后继节点绝对不可能有左子树。因此,仅从结构来看,二叉树上实质被删除的节点最多只可能有一个子树。
现在我们来看红黑性质的恢复过程:
①如果Y指向的节点是个红色节点,那么直接删除掉Y以后,红黑性质不会被破坏。操作结束。
②如果Y指向的节点是个黑色节点,那么就有几条红黑性质可能受到破坏了。首先是包含Y节点的所有路径,黑高度都减少了一(第5条被破坏)。其次,如果Y的有红色子节点,Y又有红色的父节点,那么Y被删除后,就出现了两个相邻的红色节点(第4条被破坏)。最后,如果Y指向的是根节点,而Y的子节点又是红色的,那么Y被删除后,根节点就变成红色的了(第2条被破坏)。
其中,第5条被破坏是让我们比较难受的。因为这影响到了全局。这样动作就太大太复杂了。而且在这个条件下,进行其它红黑性质的恢复也很困难。所以我们首先解决这个问题:如果不改变含Y路径的黑高度,那么树的其它部分的黑高度就必须做出相应的变化来适应它。所以,我们想办法恢复原来含Y节点的路径的黑高度。做法就是:无条件的把Y节点的黑色,推到它的子节点X上去。(X可能是NIL节点)。这样,X就可能具有双重黑色,或同时具有红黑两色,也就是第1条性质被破坏了。(这个很重要)
但第1条性质是比较容易恢复的:
一、如果X是同时具有红黑两色,那么好办,直接把X涂成黑色,就行了。而且这样把所有问题都解决了。因为将X变为黑色,2、4两条如果有问题的话也会得到恢复,算法结束。
二、如果X是双黑色,那么我们希望把这种情况向上推一直推到根节点(调整树结构和颜色,X的指向新的双黑色节点,X不断向上移动),让根节点具双黑色,这时,直接把X的一层黑色去掉就行了(因为根节点被包含在所有的路径上,所以这样做所有路径同时黑高减少一,不会破坏红黑特征)。(双黑调节就是按照这种思路:无限把双黑节点上推,直到出现第四种情况)
下面就具体地分析如何恢复1、2、4三个可能被破坏的红黑特性:我们知道,如果X指向的节点是有红黑两色,或是X是根节点时,只需要简单的对X进行一些改变就行了。要对除X节点外的其它节点进行操作时,必定是这样的情况:X节点是双层黑色,且X有父节点P。由知可知,X必然有兄弟节点W,而且这个W节点必定有两个子节点。(
因为这是原树满足红黑条件要求而自然具备的。X为双黑色,那么P的另一个子节点以下一定要有至少两层的节点,否则黑色高度不可能和X路径一致。也就是说至少两层黑。其实可以说,这就是一个递归函数的递归公式,只不过情况较多而已。可以参看july的最长子字符串序列,和这种情况类似
)。所以我们就分析这些节点之间如何变形,把问题限制在比较小的范围内解决。另一个前提是:X在一开始,肯定是树底的叶节点或是NIL节点,所以在递归向上的过程中,每一步都保证下一步进行时,至少 X的子树是满足红黑特性的。因此子树的情况就可以认为是已经正确的了,这样,分析就只限制在X节点,X的父节点P和X的兄弟节点W,以及W的两个子节点。这些个节点中。
下面仅仅考虑X原本是黑色的情况即可。
在这种情况下,X此时应该具有双重黑色,算法的过程就是将这多出的一重黑色向上移动,直到遇到红节点或者根节点。
接着往下分析, 会遇到4种情况,实际上是8种, 因为其中4种是相互对称的,这可以通过判断X是其父节点的右孩子还是左孩子来区分。下面我们以X是其父节点的左孩子的情况来分析这4种情况,实际上接下来的调整过程,就是要想方设法将经过X的所有路径上的黑色节点个数增加1。
具体分为以下四种情况:(下面针对x是左儿子的情况讨论,右儿子对称)
Case1:X的兄弟W是红色(想办法将其变为黑色)
由于W是红色的,因此其儿子节点和父节点必为黑色,只要将W和其父节点的颜色对换,在对父节点进行一次左旋转,便将W的左子节点放到了X的兄弟节点上,X的兄弟节点变成了黑色,且红黑性质不变。但还不算完,只是暂时将情况1转变成了下面的情况2或3或4。(本来x 为双黑,只要把A再涂成双黑就完事了,和我们之前说过的要把双黑往上推一样,但那样对问题根本没一点影响,还不如不做。其实旋转是可以想象到的,左边x的删除了一个黑节点,那必然导致右边重了,要左旋转)
图一
Case2:X的兄弟节点W是黑色的,而且W的两个子节点都是黑色的。此时可以将X的一重黑色和W的黑色同时去掉,而转加给他们的父节点上,这是X就指向它的父节点了,因此此时父节点具有双重颜色了。这一重黑色节点上移。(因为A下面是平衡的,所以不用改动,但x要把一层黑给顶上去,那w这边就相当于多了一层黑,所以把w的黑给去掉,成为红色。至于A怎么办,那要看A本身的颜色是什么了。如下:)
图二
如果父节点原来是红色的,现在又加一层黑色,那么X现在指向的这个节点就是红黑两色的,直接把X(也就是父节点)着为黑色。问题就已经完整解决了。
如果父节点现在是双层黑色,那就以父节点为新的X进行向上的下一次的递归。
Case3:X的兄弟节点W是黑色的,而且W的左子节点是红色的,右子节点是黑色的。此时通过交换W和其左子节点的颜色并进行一次向右旋转就可转换成下面的第四种情况。注意,原来L是红色的,所以L的子节点一定是黑色的,所以旋转中L节点的一个子树挂到之后着为红色的W节点上不会破坏红黑性质。变形后黑色高度不变。(L旋转后,左子树的性质是不会变的,但右子树多了个黑的w 就有问题了,所以要把w改为红色。其实case3是把问题转换为case4.)
图三
Case4:X的兄弟节点W是黑色的,而且W的右子节点是红色的。这种情况下,做一次左旋,W就处于根的位置,将W保持为原来的根的位置的颜色,同时将W的两个新的儿子节点的颜色变为黑色,去掉X的一重黑色。这样整个问题也就得到了解决。递归结束。(在代码上,为了标识递归结束,我们把X指向根节点) (其实是相当于 x这边多了一层黑,怎么办呢,就把A点给按下去,这样x两层黑就变为 x本身黑+A的黑。A是下去了,那自然要把w给提上来,w提上来之后L就变到左边去了。L子树保持不变,原因很简单,都是两层黑+L本身。但w提上来了,可这边少了一层黑啊,所以直接把R变为黑,ok 问题解决了。)
图四
因此,只要按上面四种情况一直递归处理下去,X最终总会指向根结点或一个红色结点,这时我们就可以结束递归并把问题解决了。
以上就是红黑树的节点删除全过程。
总结:
如果我们通过上面的情况画出所有的分支图,我们可以得出如下结论
插入操作:解决的是 红-红 问题
删除操作:解决的是 黑-黑 问题
即你可以从分支图中看出,需要往上遍历的情况为红红(插入),或者为黑黑黑(删除)的情况,如果你认真分析并总结所有的情况后,并坚持下来,红黑树也就没有想象中的那么恐怖了,并且很美妙;
linux 源码中红黑树的删除的修复
/**
* 前置环境:在node与parent之前,刚刚删除了一个黑色节点。现在树很可能不平衡,
* node与parent也可能红色冲突。
* 本函数进行树的性质的修正,以使树恢复平衡。在一些情况下问题会转移到上一层节点,
* 则须对上一层节点进行递归检查与修正。本函数中的while循环实际上实现了这种递归。
*
* 提示:这是红黑树里最绕的地方,看不明白可以多画画图。
*/
static void _rb_erase_color(rb_node *node, rb_node *parent, rb_root *root) {
/**
* other用来保存兄弟节点,这是树的修正过程中一个重要的参考节点。
*/
rb_node *other;
/**
* 循环条件:node不是红节点且node不是根节点。
* 解释:对于红节点或根节点,直接涂黑即可解决问题。
*/
while ((!node || rb_is_black(node)) && node != root->node) {
/**
* 为方便程序处理各种情况,这里和节点插入一样将问题分成了左右对称的两类。
* if-else两个分枝里代码逻辑完全相同,只是左右相反。所以我们只研究node是parent左子的情况。
*
* 在开始之前,我们先总结一下当前状态:
* 1:因为删除的是黑色节点,所以node与parent都有可能是红色节点。
* 2:node与parent之间少了一个黑色节点,则所有通过node的路径都少了一个黑色节点,不妨画图时用-1标出来;
* 但node的兄弟节点(node一定有兄弟,可以根据删除前树的平衡性质来反推)高度并未变化,可以记作0。
*
* 提示:在进行旋转、涂色等操作时,可以画图观查平衡状态的变化。
*/
if (parent->left == node) { //左边 把other换位brother更容易懂
other = parent->right;
if (rb_is_red(other)) { //case 1
/**
* 如果兄弟节点是红色,则父节点是黑色,交换父、兄节点的颜色,并对父节点进行左旋。
* 旋转后,兄节点占了老的父节点位置,且和老的父节点颜色相同,所以不会向上造成颜色冲突。
* 我们仍然以老的父节点为父节点来看,现在的状态是:
* 父节点右子保持平衡,只有经过node的路径少了一个黑色节点。
* 现在问题和之前相似,但node有了一个黑色的兄弟(还有一个红色父亲)。
*/
rb_set_black(other);
rb_set_red(parent);
_rb_rotate_left(parent, root);
/**
* other指向新的兄弟节点。other现在必然是一个黑色节点,而不会是空。这一点可以根据旋转之前树的性质反证。
递归矫正
*/
other = parent->right;
}
/**
* 此时状态:
* node有黑色兄弟,父亲可能黑也可能红。
*儿子都为黑
*/
if ((!other->left || rb_is_black(other->left)) &&
(!other->right || rb_is_black(other->right))) {
/**
* 如果other没有红色子节点,那我们就可以把other涂红,并向上转移问题。
* other涂红的后果是,other分枝少了一个黑节点,与node分枝保持了平衡,但parent整体少了一个黑色节点。
* 细心的人可能会发现,如果父亲是红色的,父亲与兄弟有颜色冲突,直接向上转移能纠正吗?当然是可以的。
* 向上一层之后,while里的黑节点判断失败,会直接执行while后面的语句,直接将parent涂黑,则树恢复平衡。
*/
rb_set_red(other);
node = parent;
parent = rb_parent(node);
} else {
/**
* 现在黑兄弟有红子节点,父亲颜色未知。
*/
if (!other->right || rb_is_black(other->right)) {
/**
* 如果黑兄弟右节点为空或为黑,则左节点一定是红的,我们想办法把它调整为右子为红。
* 至于为什么,看后面就知了。
* other->left与other交换颜色,对other进行右旋,other指向新的兄弟。根据右旋转的特点,
* 可知现在other仍然是黑色,且它有了一个红色右子。同时other分枝高度不变,颜色也没有冲突。
*/
rb_set_black(other->left);
rb_set_red(other);
_rb_rotate_right(other, root);
other = parent->right;
}
/**
* 此时状态:黑兄弟有红色右子节点。
* 不管parent是什么颜色,把other涂成父亲的颜色(之后旋转,other占据父亲的位置,向上没有颜色冲突),
* 把父亲涂黑,把黑兄的other涂黑,这时node分枝高度可能有变化也可能没变化,other分枝多了一个黑节点。
* 现在对父亲进行左旋转。旋转后的情况是右边分枝(原other右子)少了一个黑节点,重归平衡;
* 左边分枝则增加了一个黑节点,也恢复了平衡。此时也没有颜色冲突
*/
rb_set_color(other, rb_color(parent));
rb_set_black(parent);
rb_set_black(other->right);
_rb_rotate_left(parent, root);
/**
* 树已平衡,node置为根节点,并跳出循环。
* 检测root节点是否黑色
*/
node = root->node;
break;
}
} else {
other = parent->left;
if (rb_is_red(other)) {
rb_set_black(other);
rb_set_red(parent);
_rb_rotate_right(parent, root);
other = parent->left;
}
if ((!other->left || rb_is_black(other->left)) &&
(!other->right || rb_is_black(other->right))) {
rb_set_red(other);
node = parent;
parent = rb_parent(node);
} else {
if (!other->left || rb_is_black(other->left)) {
rb_set_black(other->right);
rb_set_red(other);
_rb_rotate_left(other, root);
other = parent->left;
}
rb_set_color(other, rb_color(parent));
rb_set_black(parent);
rb_set_black(other->left);
_rb_rotate_right(parent, root);
node = root->node;
break;
}
}
}
/**
* 对于红节点或根节点,直接涂黑即可解决问题。
*/
if (node)
rb_set_black(node);
}