红黑树删除操作学习笔记

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/scnu20142005027/article/details/53729117

最近在尝试实现一个简单的STL,到了红黑树那个位置,由于侯捷老师的那本书只讲了插入操作,所以只能自己找资料学习删除操作,在此总结记录一下。

首先是红黑树的几条性质,各种操作最终都要保证满足这些性质。

1、每个节点或是红的,或是黑的。
2、根节点是黑的。
3、每个叶节点(NIL)是黑的。
4、如果一个节点是红的,则它的两个儿子都是黑的。
5、对于每个节点,从该节点到其子孙节点的所有路径上包含相同数目的黑节点。

至于为什么这些性质可以保持平衡,可以参考 2-3-4 树。


接下来进入正题

删除操作大体可以分为两步,首先是删除节点,然后是如果有需要,则调整红黑树使其平衡。

下面是删除节点的伪代码。

RB-DELETE(T, z)
    if left[z] = nil[T] or right[z] = nil[T]
        then y <- z
        else y <- TREE-SUCCESSOR(z)
    if left[y] ≠ nil[T]
        then x <- left[y]
        else x <- right[y]
    p[x] <- p[y]
    if p[y] = nil[T]
        then root[T] <- x
        else if y = left[p[y]]
            then left[p[y]] <- x
            else right[p[y]] <- x
    if y ≠ z
        then key[z] <- key[y]
            copy y's satellite data into z
    if color[y] = BLACK
        then RB-DELETE-FIXUP(T, x)
    return y

删除操作类似于二叉搜索树的删除操作,这里 z 为要删除的节点,y 为实际删除的节点,x 为 y 的孩子节点,这里 z 和 y 可能相等。

大概有这么几种情况:

// x(NIL) 表示 x 可能为 NIL
// z == y

     z(=y)          z(=y)
    /    \         /    \
   x     NIL      NIL   x(NIL)

// z != y

      z             z
     / \           / \
    o   o         o   y
       / \           / \
      y   o        NIL x(NIL)
     / \
   NIL x(NIL)
最后需要根据 y 的颜色判断是否需要重新平衡红黑树。

如果 y 是黑的,会产生几个问题:

1、如果 y 是根节点,而 x 是红的,则违反了性质 2,这种情况只要简单把 x 改为黑的即可。

2、如果 y 的父节点是红的,而 x 也是红的,则违反了性质 4。

3、删除 y 节点会导致该路径上的黑节点数目比其他少 1,违反了性质 5。

如果 y 是红的,可以发现删除后路径上黑节点数目不变,即不违反性质 5,而由于此时 y 的父节点一定是黑的(否则原本的树违反性质 4),所以无论 x 是什么颜色都不会违反性质 4。


接下来是平衡红黑树。

RB-DELETE-FIXUP(T, x)
    while x ≠ root[T] and color[x] = BLACK
        do if x = left[p[x]]
            then w <- right[p[x]]
                 if color[w] = RED
                     // case 1
                     then color[w] <- BLACK
                          color[p[x]] <- RED
                          LEFT-ROTATE(T, p[x])
                          w <- right[px[x]]
                 if color[left[w]] = BLACK and color[right[w]] = BLACK
                     // case 2
                     then color[w] <- RED
                          x <- p[x]
                     else if color[right[w]] = BLACK
                              // case 3
                              then color[left[w]] <- BLACK
                                   color[w] <- RED
                                   RIGHT-ROTATE(T, w)
                                   w <- right[p[x]]
                          // case 4
                          color[w] <- color[p[x]]
                          color[p[x]] <- BLACK
                          color[right[w]] <- BLACK
                          LEFT-ROTATE(T, p[x])
                          x <- root[T]
        else (same as then clause with "right" and "left" exchanged)
    color[x] <- BLACK

根据上面的分析,删除黑节点后会导致树至少违反性质 5。

但是有一种办法可以保持性质 5,就是把 y 的黑色推到 x 身上,也就是说 x 可能是黑黑或者黑红,这样违反了性质 1,而上面的算法可以用来恢复性质 1,一共有八种情况,这里只列举了四种情况(其余四种反向处理),下面这张图中,x 恒为具有双重颜色的节点。

注意,以 case2 的右边那张图为例,这里 B 的颜色偏淡,代表颜色不确定(可红可黑),而 D 的颜色才代表红色。


case1: x 的兄弟 w 是红的

这种情况下,对 w 和 x 的父节点做一次左旋转并改变颜色,旋转后依然保持性质 5。那么问题是,为什么要旋转呢?注意,这时候 x 的兄弟 w 变成了 C 节点,而且是黑的。这里旋转的目的就是让 x 的兄弟节点变为黑的。那么为什么要将 x 的兄弟节点变成黑的呢?目的是形成 case2、case3 或 case4。

case2:x 的兄弟 w 是黑的,且 w 的两个孩子都是黑的

这种情况是最简单的,由于 w 是和两个孩子都是黑的,所以可以将 x 和 w 都去掉一层黑色,将这层黑色转移给父节点,也就是 x 变为黑的, w 变为红的,并将父节点(具有双重颜色)作为新的 x 节点继续处理。此时如果父节点原本是红的,则退出外层循环,然后将新的 x 节点(即父节点)变为黑的,完成平衡操作。如果父节点原本是黑的,则父节点为黑黑,继续循环。也就形成了 case1、case2、case3 或 case4。

case3:x 的兄弟 w 是黑的,且 w 的左孩子是红的,右孩子是黑的
这种情况下,对 w 进行一次右旋转,旋转后依然保持性质 5。这里旋转的目的是让 w 的右孩子变成红的,也就是形成 case4。

case4:x 的兄弟 w 是黑的,且 w 的右孩子是红的

这种情况下,对 w 和 x 的父节点做一次左旋转并改变颜色,旋转后 E 节点变为黑色,A 节点增加了一个黑色父节点(路径上的黑色节点数目加 1),也就是此时 A 节点可以去掉一层黑色且不破坏性质 5 同时恢复了性质 1。最后将根节点赋值给 x 用以结束循环。

总的来说,case1 和 case3 的操作都是为了形成其他情况,而位于 case2 时,改变颜色后,如果父节点原本为红,则改为黑色结束平衡,原本为黑则继续循环,直至到达根节点,此时可以简单地去掉根节点的一重黑色而不破坏性质 5。case4 则巧妙利用节点间的关系,通过旋转和改变颜色,使左侧多出一个黑色节点,即令 x 节点可以去掉一层黑色,保持性质 5 且恢复性质 1。


参考资料

- 算法导论(原书第3版)

- STL源码剖析---红黑树原理详解下

猜你喜欢

转载自blog.csdn.net/scnu20142005027/article/details/53729117