红黑树删除本质 与 伪代码分析

来源:互联网 发布:淘宝账号查询购买记录 编辑:程序博客网 时间:2024/06/10 09:03

性质1. 节点是红色或黑色。
性质2. 根是黑色。
性质3. 所有叶子都是黑色(叶子是NIL节点)。
性质4. 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
性质5. 从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点


红黑树的删除操作是在二叉树删除操作后(加入Nil[T]节点,并修改NULL判断逻辑),加入调整函数,使得新的二叉树符合红黑树特性。

这是总结文章,建议先看算法导论相关章节。 

算法实现过此在下文中讨论

http://blog.csdn.net/xzongyuan/article/details/22790769

http://blog.csdn.net/xzongyuan/article/details/22934103


下图是《算法导论》中的截图,其实我觉得这本书已经讲得比网上任何资料都清楚了,但我还是要把一些大家可能会碰到的疑问写一下。



图1

 
基础知识(二叉树删除)

首先,红黑树删除,本质是二叉树删除,所以,一定要先弄明白二叉树删除是怎么删的。简单介绍下,删除一个节点,要拿一个节点去补原来的位置,如果被删除的节点,本来是叶子节点,那就好办,直接删除,如果它有一个孩子,就让孩子取代他的位置(指针实现方法是让父节点的孩子指针不再指向自己,而是指向被删除节点的孩子),如果它有两个孩子,就不是直接让孩子继承位置了,而是找“后继节点”。

后继节点可以从左子树或者右子树中找,大家知道,二叉树是根据key值大小排序拍出来的,顺序是左孩子key<根节点key<右孩子key。因此,后继节点的特征是,它的key是和被删除节点的key是相邻的。如1,3,5,7,9,11.如果你删除7,则后继节点就是9(前趋节点是5),而这体现在树上的位置就是:右子树最小的值;也可以图形化地表达为:右子树最左边那个节点。可见,找到后继很简单,在右子树,一直遍历找寻左子树,直到某个节点没有左孩子为止。


性质调整

红黑树的特点是它有5个性质,我们删除一个红黑树节点,不但要解决找顶替节点的问题,还要保证红黑树性质不变。然而,性质只是我们的规定,它的意义是很简单的,即通过黑树高度去保证左右子树高度大致平衡,不会因为添加删除操作,影响到树的左右平衡(高度不差太大)。如下图,我用函数打印了一个二叉树,oo代表叶子节点,你可以看到,右边子树高度为1,左边子树高度为3,普通的二叉树,随着树的深度提高,左右子树的高度就越来越不平衡。

重点来了:红黑树的目的就是保持树高平衡,而平衡的办法就是旋转,旋转的依据是黑树高度。而5个性质就是一些技巧,去实现这个目的。下面讨论下这些性质的意义

图2


1.首先,我要提一下核心在于保持黑树高度平衡,红黑颜色只是一种标识。

2..性质4是该算法最核心的,让红色节点不能有红色孩子,它有什么意义?首先,它不需要计入黑色高度中。其次,在左旋转(给左子树增加一个节点)操作中,可以标识增加的黑色节点。这时结合性质5,就可以保证在增加删除操作后,保证某个根节点的黑高度不变。要注意,这里不变的只是黑色高度,整棵树的高度是不保证的,可能会因为红色节点数的增加或减少,树高有变化。这也体现了区分红黑颜色的意义,只需要关注黑色的高度,通过黑色节点保证树的平衡。红色只是辅助作用。(个人见解,仅供参考)。

3.算法导论中,删除红黑树后,为了维持着5个性质,提出了4个case。其中,还隐含两个简单的case。总共6个case。

case-1. 删除的是红色节点,如我提到的,它是辅助节点,删除了并不影响5个性质。红色节点的父节点和孩子节点必定是黑色,两个连着的黑色是允许的。不需要调整。

case0.  删掉了黑色节点,但它是根节点,那也没啥好做的了。不需要调整。

这两个case很好理解,不需要调整。就不列入讨论范围了。


难点在于余下的4个case,case1,2,3,4如图1。删除的节点是黑色节点,黑色高度变化了,需要调整。

为啥要区分4个case?算法导论和网上的文章都没回答这个问题,让我看得晕头转向。让我分解说明下:

a.case1的目标是把红色兄弟的情况转为黑色兄弟,由此问题被转化为case2,case3,case4。这就好像下棋,我们需要考虑到的是下一步的目的是什么,而不是仅仅思考这一步做了什么。我解释下,最终目的就是:补充以x为根的树的黑色高度。要知道,x所在的位置,是原先被删除的节点位置,x是被删除节点的后继节点。所以现在的以x为根的树,其实少了一层黑色高度。这也是为啥在伪代码中,要以x为调整的轴心节点(FIXUP)的原因。

case1的目的是转为case2,3,4,通过2,3,4来解决问题。因为兄弟是红色的,根据性质4,父亲肯定是黑色的,所以它可以把兄弟拿过来当父亲节点,又因为兄弟是红色的,它从右边移到左边,不会影响父亲节点的黑树高度。可见,case1似乎做了一个无用功,为啥要这样做?它的目的是为了把case1转化为case2,3,4来讨论。看来,真正解决问题的是case2,3,4。还要补充下,因为兄弟节点是红色,根据性质4,兄侄节点肯定是黑色,所以把兄弟拿过来作为父节点(通过左旋),实际上左兄侄成了自己的新兄弟了。而且,如前所述,这个新兄弟一定是黑色的。这就铁定转入了case2,3,4(兄弟是黑色)的情况。

b.case2,如果黑兄弟有两个黑孩子,则直接把黑兄弟设为红色,并把x指向父节点p[x]。这又是为啥?见图1,不要忘了,这4个case的目的是“调整”。而需要调整的一个先决条件是x必须是黑色——因为只有黑色节点被删除了,才会需要调整,而如果是黑色节点被删除,则那个位置的父节点和孩子都很可能是红色,如果x是红色,则会破坏性质4,所以还是直接设为黑色比较保险。所以,case2后结束后,会跳出这4个case的循环,伪代码中是while循环。然后把红色的x节点设为黑色。

c.说明case3前,提醒下:虽然图中显示着A,B,C,D,E,F节点名称,实际上你可以忽略他们,每个case的字母没有必然联系。如果你太注重字母,会被绕进去。例如case1的C,实际上就是case2的D。case1中,根节点的右子树部分,实际上在case2中是忽略了,没有画出来。而且4个case举的例子都是内节点,即根节点只是子树的根节点,而不是实际整棵树的根节点。图中的叶子节点还有阿拉伯数字,代表着子树,这些阿拉伯数字代表的子树可以是深度很高的子树。

d.case3,黑兄弟+红左侄+右黑侄,它的处理目标是转成case4(黑兄弟,红右侄)。干嘛又区分个左右侄子,还要区分颜色?这么麻烦有什么意义?它的目的是借用左红侄的颜色,给右黑侄,把这种特殊的状态转化为case4,统一处理。case3,只是把一个特殊情况(左红侄右黑侄)转为普遍情况(case4右红侄),向答案走近了一步。这样,侄子的所有状态都涉及了,双黑侄子对应case2,左红右黑对应case3,case4涵盖两种情况:右红左黑 和 双红侄,简单说就是红右侄。到这里,应该能理解如下伪代码了

     //case3     Else if color[right[w]]=BLACK         //如果左侄子不是黑节点,则设置为黑节点,为了转向case4做准备。         Then color[left[w]]<-BLACK         color[w]<-RED          RIGHT-ROTATE(T,w)         w<-right[p[x]]   //获得新兄弟

e.case4,黑兄弟+红右侄。要注意,这时候,左侄子的颜色已经不重要,可以是红色也可以是黑色。它将从右边被移到左边,且新旧父节点都是黑色,并不改变祖父节点的黑树平衡。 如下图所述,父节点和兄弟交换颜色,注意这幅图的父节点和左侄子的颜色是任意的,即case4的操作与这两个节点的颜色无关。但必须保证这两个颜色不要被任意改动,所以,父节点其实是根位置,我们调整的目的也是这个位置的黑树高度要左右平衡。既然C取代了B成为新的root,那么,它就要和B交换颜色。case4会执行左旋操作,为root的左子树增加一个黑树高度,以弥补A缺失的一个高度。当然,root位置的颜色要保持,因为我们的目的是给左子树增加一个黑节点,如果搞错了root本身的颜色,就坏事了。

然后是右侄子的颜色处理,为了保证root的右子树高度,D要继承C的黑色。

这时,应该能看懂伪代码的意思了

     //case 4     color[w]<-color[p[x]]     //父节点的颜色给兄弟     color[p[x]]<-BLACK //父节点设为黑色     color[right[w]]<-BLACK  //兄弟的右子树设为黑色     LEFT_ROTATE(T,p[x])     x<-root[T]       //x指向root,跳出while循环,下一步代码有color[x]<-BLACK,所以这里为了下一步把root置为黑色。

补充下,我说的root,其实就是图中的根节点,它是一个抽象概念,不管你怎么左旋右旋,最终那个树的根节点就是root。这样比较好定位。我们平衡黑树高度,看的就是这个抽象的root,它是不变的。而我们看别人说的例子,经常说把父节点变成左子树,把右节点变成根,都是表象。我们只需要关注root的位置是不是平衡,不需要关注哪个节点是root。就如我的分析过程,我并没关注C还是B是root。





红黑树删除伪代码(z是即将被删除节点。Y最初指向z,指向当前指针,最终是要删除节点的指针,可能是后继节点,x后继节点的“下一级”节点。删除后继节点后,x取代y,然后从x开始调整颜色)

删除伪代码简要分析

为保持伪代码整洁,详细分析在后面

If left[z]=nil[T] or right[z]=nil[T]

     {

          Then y<-z   //只要有一棵空子树,y则指向z,不找后继节点

         Else y<-Tree-successor   //如果有两棵非空子树,则y指向后继节点

     }

If left[y]!=nil[T]   //开始往下寻找遍历

        {

        Then x<-left[y]

        Else x<-right[y]    

}

P[x]<-p[y] //y的父节点作为子树x的父节点,准备删除y

If p[y]=nil[T] //如果y的父亲是nil,说明y是根位置。

       Then root[T]<-x       //x放在根位置

Else if y = left[p[y]] //x取代y的位置

     Then left[p[y]]<-x

      Else right[p[y]]<-x

//这一段操作主要就是用x取代y 

If y!=z   //如果删除的yz指针不一样,说明yz的后继。这在第三行代码tree-successor中赋值

Then key[z]<-key[y]

    Copy_Ydata_to_z    //把后继的内容赋给z

If color[y]=BLACK  //删除节点是黑色节点

Then RB-DELET-FIXUP(T,x)  //到这里,y已经被x取代,所以用基于x去调整

Return Y


 详细分析

If left[z]=nil[T] or right[z]=nil[T]  Then y<-z  //只要有一棵空子树,y则指向z,不找后继节点  Else y<-Tree-successor   //如果有两棵非空子树,则y指向后继节点If left[y]!=nil[T]  //开始往下寻找遍历  Then x<-left[y]  Else x<-right[y]   //找到删除节点y的子树x,进行下一步操作。先找左子树,如果没有再找右子树.//注意,这里的y有两种可能://有一棵空子树,则是删除节点z//有两棵非空子树,则是删除节点z的后继节点。P[x]<-p[y]//把y的父节点作为子树x的父节点,准备删除y。If p[y]=nil[T]//如果y的父亲是nil,说明y是根位置。  Then root[T]<-x       //x放在根位置  Else if y = left[p[y]]//用x取代y的位置      Then left[p[y]]<-x  Else right[p[y]]<-x//这一段操作主要就是用x取代y。Y的两种情况的操作都是一样的,都是把p[y]给p[x],把x给left[p[y]]或者right[p[y]].If y!=z//如果删除的y和z指针不一样,说明y是z的后继。这在第三行代码tree-successor中赋值  Then key[z]<-key[y]     Copy_Ydata_to_z    //把后继的内容赋给z。If color[y]=BLACK //删除节点是黑色节点  Then RB-DELET-FIXUP(T,x)  //到这里,y已经被x取代,所以基于x去调整Return Y


为啥删掉红色节点就不需要调整,因为:

1.黑色高度没变

2.相邻两个节点是黑色节点,没有连续的红色

3.如果y是红色,就不会是根,所以根是黑色的性质不会被改变。


传递给调整函数FIXUP的x节点分两种情况:

1.x是y的一个非nil孩子节点。

2.如果后继节点的两个子树是nil,则传递过去的x是nil节点。


下面算法中,假设原先删掉的节点的黑色给了x,这样x有两个黑色(其中一个是抽象的,不需要实现,设计人员记在心里就行了),while循环的目的是把这个黑色移到一个红色节点,然后着色为黑色或者x指向树根。

在while循环中,x总是指向有双重x颜色的那个非根节点。当x指向根节点时,表明根节点双重黑色,这不影响树平衡,则循环结束。可见,x的含义是指向有双重颜色的节点。

这种把x看作双重颜色的思想,可以理解while循环的操作的意义。但是我认为在理解rotate时,最好用我原先建议的,从黑色高度去理解。rotate是为了增加某一边的黑色高度。例如case4最后的left rotate,就是给x那一边增加了一个黑色高度,从而平衡了被删除的黑色节点高度。双重颜色的思想是把x 移到红色节点,然后着色,其结果也是增加一个黑色节点。这个上移黑色节点的过程就体现在case1和case2。case2完成后,new x位置就上移了一层。且跳出while循环后,被着色为黑色,这样,x这边的黑色高度就增加了1.

RB-DELETE-FIX-UP(T,x)  ——  x是被删除节点的子树,且已经更新为后继或子树的位置While x!=root[T] and color[x]=BLACK    //如果x是红色节点,则退出while,然后把x染黑,就可以增加一个黑色高度  Do if x=left[p[x]]  {      Then w<-right[p[x]]          //如果新x是左子树(则原来的y所在位置),则设置w为兄弟节点为右子树      If color[w]=RED//如果兄弟节点是红色      //case1          Then          {color[w]<-BLACK//兄弟节点设为黑色(x也是黑色的)           Color[p[x]]<-RED//父节点设为红色           LEFT-ROTATE(T,p[x])           W<-right[p[x]]          //旋转后,兄弟已经变为旧兄弟的孩子,通过parent获得新兄弟          }       If color[left[w]]=BLACK and color[right[w]]=BLACK          //case2          Then color[w]<-RED          X<-p[x]     //case3     Else if color[right[w]]=BLACK         //如果是右黑侄,左红侄,则设置左红侄为黑节点,为了转向case4做准备。               Then color[left[w]]<-BLACK               color[w]<-RED                RIGHT-ROTATE(T,w)               w<-right[p[x]]         //case 4 红色右侄子               else             color[w]<-color[p[x]]     //父节点的颜色给兄弟             color[p[x]]<-BLACK  //父节点设为黑色             color[right[w]]<-BLACK  //兄弟的右子树设为黑色             LEFT_ROTATE(T,p[x])             x<-root[T]//x作为root。执行完case4后,整颗树已经平衡,设置x为root[T],则下一次while循环会跳出来。可见其作用是表示while结束。  }  Else(和上面逻辑对称)Color[x]<-BLACK


0 0