红黑树的插入操作--伪代码详细分析

学习的时候,最好把性质copy到记事本中,当别人提到性质1,2,3,4,不用翻来翻去。下面的是我学习《算法导论》的笔记。

算法实现在这两篇

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

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

性质1. 节点是红色或黑色。

性质2. 根是黑色。

性质3. 所有叶子都是黑色(叶子是NIL节点)。

性质4. 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)

性质5. 从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点

红黑树插入是基于二叉树插入进行改进,在最后加入红黑树性质调整函数 RB-INSERT-FIXUP。

插入伪代码(因为简单,就不细说)

RB-INSERT

y<-nil[T]

x<-root[T]     //x是当前指针(最终是插入位置),y是上一级指针(最终是插入位置的父节点)

while x!=nit[T]

    do y<-x    //顺着x往下找,直到x为nil节点.

         if key[z]<key[x]

           then x<-left[x]

           else x<-right[x]   

//x的作用到此结束,就是为了找到nil叶子节点而已,后面的都依赖上一级节点y

//下面的操作都是针对叶子节点进行判断。叶子节点的孩子和根的父节点都是黑色Nil哨兵,但不需要计算其Nil黑色高度,它的作用只是作为一个边界。Nil是只有颜色的节点,parent/left/right/key都是NULL或者任意其它值。而且,在红黑树图示中,一般不画出Nil,nil只是在算法设计的时候用到。

p[z]<-y

if y=nil[T]

   then root[T]<-z  //如果上级节点是nil,则插入的位置x是root位置

   else if key[z]<key[y]

       then left[y]<-z

       else right[y]<-z

   left[z]<-nil[T]                //下面三步,给该叶子节点设置哨兵Nil,并把z设为红色(插入节点默认都是红色)。

   right[z]<-nil[T]

   color<-RED     

RB-INSERT-FIXUP(T,z)

插入后恢复红黑树性质的伪代码

RB-INSERT-FIXUP(T,z)     //红黑树规定插入节点是红色,而插入红色节点只会破坏性质24

while color[p[z]]=RED        //只有父节点是红色时,才会破坏性质24。

     do if p[z]=left[p[p[z]]]       //如果父节点是祖父节点的左子树。

      then y<- right[p[pz]]       //获得叔父节点 

         if color[y]=RED  

         {

              //case 1

             then color[p[z]]<-BLACK    //父节点改为黑色(取反)case 1

                  color[y]<-BLACK       //叔父节点改为黑色(取反)case 1

                  color[p[pz]]<-RED     //祖父节点改为红色(取反) case 1

                  z<-p[pz]    

         }    

         else if z=right[p[z]]           //case2:如果z是右子树,且叔叔是黑节点,则左旋为左子树。

                 {

                   then z<-p[z]                 //case2:指针改为指向z的父节点

                   LEFT_ROTATE(T,z)  //case2:以父节点为轴左旋。注意,左旋后,z的叔叔节点还是黑色的。

                  }

       //如果z是左子树,则不需经过左旋

             color[p[z]]<-BLACK    // case 3:父节点颜色取反

             color[p[p[z]]]<-RED     // case 3:祖父节点颜色取反

             RIGHT-ROTATE(T,p[p[z]]) //case 3:

         

    else(如果父节点是祖父节点的右子树,则与上面过程对称)             

color[root[T]]<-BLACK

总结:case1,2,3中颜色取反是为了保证特性4,左旋右旋是为了保证性质5,即两边子树的黑高度相等。加入了一个红色节点,必然会增加加入那边的树高度,利用这点,通过旋转,把高度平均起来——红黑树的目的就是平衡树的高度。而红色的节点是辅助作用,它的作用就是可以被转化为黑色。黑色节点才是主角,它通过黑色高度的平衡来保证整棵树的平衡。所以,加入红色节点后(红黑树默认插入的节点是红色),往矮一点的那边旋转,如加入的是右子树B,则左旋,这样就减少了右子树的红色节点,但一次旋转只会增加“树根”C的左子树的红色节点,因此,还得右旋,减少树根左边的红色节点(A和B)。到这里,你可能会有疑问,为啥要把红色节点变来变去?这是为了下一步:颜色取反。调整颜色的思想是,在适当的位置增加红色节点,然后取反,以保持整棵红黑树性质。所以,旋转是为了把红色节点放到适当的位置,但进一步的目的是为了取反颜色,而最终目的则是保持红黑树性质。

设计思想:增加一个节点,意味着树的平衡被破坏。这个被破坏的位置用新插入的红色节点z来表示。那么,保持整棵树的平衡转化为保持红黑性质,只要符合红黑树的5个性质,树高就能保持“基本”平衡。性质5决定了,你每插入一个节点,都要平衡两边子树的树高,而性质4是保证黑树的深度是可以作为比较标准。为什么不用全黑节点?我觉得可能是因为,用了红色节点,就可以标识插入节点z,从而z位置发生变化时,我们可以跟踪其对树高的影响。

如果发现两个连续的红色,则要调整,而我们不能单纯地把某一个红色颜色取反为黑色,因为这样做会增加单边(子树)的黑色节点树高度,导致性质5被破坏。但肯定是要把某个红色节点取反,才能保持性质的。这里有两种办法:

1.交换子树的root节点及root的孩子节点的颜色来保证树高。case1就是这样做,它并没有增加黑色节点,只是交换了树根C和左右孩子的颜色(注意是3个节点一起取反)。

《红黑树的插入操作--伪代码详细分析》

2.我们可以用改变树结构的办法来实现。如下图case2左旋得到case3,case3右旋然后取反颜色.

如下图case2到case3.

《红黑树的插入操作--伪代码详细分析》

我提出一个疑问:为啥case2不直接右旋呢?左旋作用在哪里?

答:右旋意味着A要作为B的根,但是A比B小,B不能作为它的左子树,只能放到A的右子树,这样,A的左子树高度必然变得很小。所以,左旋的本质是让A变为B的左子树,A的key本来就是比B的key小的。左旋并没有改变A的高度,只是增加了B的高度。这样,再次右旋时,B的高度又恢复了。整个旋转过程,C的高度也没有变化,它一直以右子树的高度为参考。可见,左旋是调整key大小关系所必须的。

case1,2,3共同点是,root位置的黑树高度bh并没有改变。通过上面两种技巧,就保证了bh的平衡。要注意,图中的ABC都是内节点,case2中的A和B不一定是叶子节点,可能有子树,而C不一定是根,它可能是子树的根。这表明这个调整算法是适用整棵树的任何位置的。

 

下面是伪代码逐项分析

先看红黑树全貌

《红黑树的插入操作--伪代码详细分析》

伪代码详细分析

RB-INSERT-FIXUP(T,z)     //插入红色节点只会破坏性质24。

while color[p[z]]=RED    //只有父节点是红色时,才会破坏性质24。如果插入的位置的父节点是黑色,因为插入的是红色节点,并不会改变黑色高度和性质4。其它特性也能保证。

//因为父节点也是红色,一定还有黑根节点,因此一定有祖父节点p[p[z]],所以 可以访问祖父节点的子树

     do if p[z]=left[p[p[z]]]   //如果父节点是祖父节点的左子树。

      then y<- right[p[pz]]     //获得叔父节点

         //case 1

         if color[y]=RED  //如果叔父节点也是红色,只需要让自己.父节和叔父节点颜色取反,就可以保证性质4. 

         {

                  then color[p[z]]<-BLACK    //父节点改为黑色(取反)case 1

                  color[y]<-BLACK       //叔父节点改为黑色(取反)case 1

                  color[p[pz]]<-RED     //祖父节点改为红色(取反)(前面操作破坏性质5,因此要取反祖父节点以保持性质5)case 1

//上面让祖父节点变为红色 ;这时,祖父的内节点已经符合红黑树特性。

//但祖父自己和祖父的父节点又违反了性质4.此时,指针z上移2层,结束if-else判断,重新while循环

//新的z节点(即上一次的祖父节点),可能是case1(红色叔父)/case2(黑色叔父),三种情况。

//可见,每次while循环,都当作单独case分析,没有相关性。我们只需要关注判断条件:

//1.叔父节点颜色 2.左旋(自身是右孩子) 3.右旋与2相反.

                z<-p[p[z]]            //case 1

//让插入点指向已被改为红色的祖父。以旧祖父节点(case1)为调整节点z

         }

《红黑树的插入操作--伪代码详细分析》

Case 1

             else if z=right[p[z]]   //case2:如果是黑色叔父,则通过旋转调整

             {

               then z<-p[z]   //case2:如果是右子树,则以其父节点为转轴左旋(其实,这表明右子树多了一个单位黑色高度,要给左子树补上一个单位高度)

             LEFT_ROTATE(T,z)

//case2:z经过左旋后,成了左子树。但zp[z]还是红色的,p[z]两遍的黑树高度还是不相等。稍后通过取反p[z]和p[p[z]]来获得平衡。 

//case2:这时,zp[z]都是红色,叔父是黑色,z是左孩子。这时候,只需要右旋解决问题,转为case3

//注意,左旋后,z的叔叔节点还是黑色的,只是z从右边到了左边。(整个过程是z指向父节点,然后父节点被左旋成为左子树,所以z从右边转到左边)

 

《红黑树的插入操作--伪代码详细分析》

注意,左旋后,还要把原来的p[z]改为黑色

//case 3:后续步骤是右旋,且是最终处理,右旋会把p[z]变为树根,而此时它是红色,所以要先变为黑色,为了保证性质4,p[p[z]]相应的改为红色

               color[p[z]]<-BLACK // case 3:

               color[p[p[z]]]<-RED // case 3:

              RIGHT-ROTATE(T,p[p[z]]) //case 3:

            } //end else if z=right[p[z]]

else(如果父节点是祖父节点的右子树,则与上面过程对称)

 //最后把根节点着色为黑,保持性质2                 

color[root[T]]<-BLACK

 

case2.z的叔叔y是黑色的,且z是右孩子

case3.z的叔叔y是黑色的,且z是左孩子

结论,在叔叔y是黑色的时候,z是右孩子则左旋,是左孩子则右旋.如果叔叔y是红色的,则执行情况1,取反祖父/父节点和叔叔的颜色。

 

 书中的这副图在颜色取反时候,有误,没必要反复把2和8着色,而且伪代码也没有对z和它的兄弟节点着色,只是把他们旋转。它从头到尾都是那个颜色。颜色变来边去容易误导理解,所以本文并没有引用。不需要这幅图也能理解三个case。

《红黑树的插入操作--伪代码详细分析》

    原文作者:算法小白
    原文地址: https://blog.csdn.net/xzongyuan/article/details/22389185
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞