解析《算法导论》中红黑树的插入和删除算法

  首先红黑树是一种接近平衡的二叉查找树,它的的性质如下:

  1. 每个结点或是红的或是黑的。(也就是说,delete一个黑色结点y之后x结点带有双重颜色红黑或者黑黑,那么应该去掉一层红色或黑色,直接将x涂黑或者在根结点到x的路径上增加一个黑色结点,增加的方法后面会提到)
  2. 根结点是黑的。(这是一个必须注意的性质,因为空红黑树根结点是nil哨兵结点,哨兵结点必须是黑色的)
  3. 每个叶节点(nil结点)是黑的。(nil一定是黑色的,nil是红色的,将有可能会违反性质4
  4. 如果一个结点是红色的,则它的两个儿子都是黑的(也就是说红黑树中不可能出现连着的两个结点是红色的情况,一条路径至少是全黑的,至多是红黑交替的,也就解释了红黑树为什么是一种伪平衡的二叉树)
  5. 对每个结点,从该结点到其子孙结点的所有路径上包含相同数目的黑结点。(这里应该注意,路径应该指该结点到叶节点(nil结点)的路径,也就是说假如有这样一颗红黑树:
  实际路径应该有三条:15-3-nil25-3-4-nil35-6-nil,不会出现这样的红黑树:某个结点有一个nil的黑色的儿子结点,另外一个为黑色的nil结点)
  这5条性质是非常重要的,是insert-fixupdelete-fixup算法的基础。
 
  在介绍红黑树的插入,删除算法前先介绍红黑树左旋,右旋算法:
  
  图示即为红黑树的左旋和右旋算法,具体实现不多说了,左旋和右旋用在红黑树的insert-fixupdelete-fixup算法中,因为无论insert还是delete操作,都可能会造成红黑树的性质被破坏,insert可能会破坏性质4,插入一个红色结点z之后,如果z的父亲结点是红色,我们应该将z的父亲结点涂黑,这时根结点到z的父亲结点这条路径将会增加一个黑色结点,而delete算法中,删除一个黑色结点y后,将会造成根结点到代替y结点的x结点的路径的黑色结点数目减少一个黑结点,而左旋和右旋,则是通过在一条路径上增加和减少结点,再通过一定数目结点颜色的改变,修复了insertdelete算法。(以图示为例,左旋在根结点到α结点的路径上减少了一个结点y,在根结点到结点γ的路径上增加了一个结点x,而根结点到β结点的路径中的结点不变只是改变了xy结点的顺序,如果x结点是红色的,y结点是黑色的,那么根结点到结点α的路径的黑高度减少1,根结点到γ结点的路径的黑高度不变,根结点到β结点的路径的黑高度不变,再想想insertdelete算法造成的后果,是不是可以用这条性质巧妙的去修复呢?)
  下面将重点介绍红黑树的insertdelete算法:
 
  insert算法:
  主体思路前面基本同二叉查找树,就是从根结点向下依次比较每个结点,小于则去比较左儿子结点,大于则去比较右儿子结点,直到要比较的结点变成nil,这时候在当前位置插入一个红色的结点zinsert算法的伪代码如下:

  插入之后可能出现z的父亲结点为红色结点的情况,违反了红黑树的性质4,这时需要调用insert-fixup算法来进行修复,insert-fixup的伪代码如下:


  伪代码给出了三种情况,无论哪种情况,要么是为了达到平衡两个结点路径黑结点数目的目的,要么是通过变形来间接达到这个目的。以z->pz->p->p的左儿子为例,让yz的叔叔结点:
  1)如果y结点是红色的,则有case 1如下:

  在case 1中,首先将z->p涂黑,再将z->p->p涂红,最后将y涂黑。将z->p的颜色和z->p->p的颜色对换一下,再将y涂黑,其实是把z->p->p的黑色分发到两个红色儿子结点中,而其自身变为红色,维持了性质5,即维护了所有路径黑结点数量的一致性。这里要提出的一个小细节是,红色结点变黑色不用考虑结点颜色冲突,而黑色结点变红色则要考虑结点颜色冲突,红变黑,随意变,黑变红,看冲突(不考虑性质5的前提下)。因为z->p->p是由黑色变红的,这时将z指向z->p->p,如果不出现结点颜色冲突的情况则完成修复,有颜色冲突则进入下一轮循环。

  2)如果y结点是黑色的,y应该是nil结点,如果zz->p的左儿子的话,先分析case 3如下:
  在case 3中,首先也是将z->p涂黑,z->p->p涂红,这时候,我们就会发现根结点到y结点路径中的黑结点数目减少了1,我们再回顾一下前面对左旋、右旋方法的介绍,那么我们会发现,左旋、右旋的意义就在于此了:RIGHT-ROTATE(T,z->p->p)后,为根结点到y结点的路径上增加了一个黑色结点z->p,为根结点到z结点的路径上减少了一个红色结点z->p->p,一条路径增加黑色结点,另一条路径减少红色结点,insert就这样被修复了。
  3)如果y结点是黑色的,y应该是nil结点,如果zz->p的右儿子的话,在分析了case 3的基础上分析case 2如下:
  在case 2中,前面我们按照case 3的做法,将z->p涂黑,z->p->p涂红,这时候,想对红黑树进行修复的话,你会想到什么呢?和case 3一样直接RIGHT-ROTATE(T,z->p->p)么?如果直接RIGHT-ROTATE(T,z->p->p)的话,红色结点z将变成红色结点z->p->p的左儿子,其实是做了无用功。那我们就想办法把它变成case 3的那种形式吧,LEFT-ROTATE(T,z),很容易想到吧,LEFT-ROTATE(T,z)之后zz->pz->p->p又变成了我们喜闻乐见的三点一线的形式,也就是case 3
  delete算法:
  主题思路基本同二叉查找树,就是结点z有至多一个儿子时,让y=z,然后删除结点y用结点y的儿子xx可以是nil,这时nil->p可指向z->p,体现了哨兵的作用)去取代结点yz有两个儿子时,删除结点z的后继y,并用y的唯一的儿子结点x取代y

 

  无论哪种情况删除的都是结点z,而y代表了被“删除”的结点的位置,无论yz本身还是z的后继结点,y原来位置的结点都被删除或者移走了了,而x则代表了取代y位置结点的那个结点(x结点可以是哨兵结点nil,这时nil就体现了它作为哨兵结点的作用,因为nil->p=y,指向了被“删除”结点的位置),被删除的结点y为黑色,那么根结点到x结点的路径(xnil结点时,也与性质5相吻合)中的黑色结点数目将会减少1RB-DELETE-FIXUP(T,x)

 

  初看RB-DELETE-FIXUP算法,千万被算法中的4case吓到了,我们只要记住一点,无论哪个case,要么是为了达到平衡两个结点路径黑结点数目的目的,要么是通过变形来间接达到这个目的,只需抓住每种情况的特点逐一击破就可以了。令w结点为x的兄弟结点,以x结点为左儿子为例:
  1)如果w的右儿子颜色为红色,则有case 4如下:
  路径rootx的黑结点数少1,这时候我们调换x->pw的颜色,并将w->right涂黑,再进行一次左旋得到下面的红黑树:
  可以发现,得到的红黑树对RB-DELETE操作成功进行了修复,所以说以x->p为根结点的子树不满足这一形式时,应该通过一定的变形和一定数目结点颜色的改变,来满足这一形式。
  2)如果w的左儿子结点为红色,右儿子结点为黑色,有case 3如下:
  这时候我们应该想如何将case 3变为case 4中的那种形式,还要维持红黑树的性质,我们看到w->left是红色,那么我们就将其涂黑,然后将w涂红,再对w进行右旋,得到:
  变为case 4的那种形式,即可进入case 4对红黑树进行修复操作。
  3)如果w为红色时,则有case 1如下:
  这时,将w涂黑,将x->p涂红,然后进行左旋,得到的以w为结点的红黑树如下:
  进行变形之后不会改变每条路径的黑色结点数目,这时将w重新做指向,令w=x->p->right,这样w变成了黑色结点,在下一轮循环时可能进入case 234。
  4)如果w的两个儿子为黑色,则有case 2如下:
  这时,将w涂红,root结点到x->p为根子树的每个叶子结点的路径将比其他路径少的黑结点数目少1,将x->p设为x,若x为红结点,将其涂黑即可成功修复二叉树;若x为黑结点,即进入下一轮循环,可能出现case 1234。如果连续出现的是case 2其结果就相当于最后在除root到最初的x结点的每条路径上减少一个黑色结点,直到xroot结点,结束循环。
 
  case之间的状态转移如下:
  • 1->234
  • 2->1234,修复(只有case 2是将x上移,因此case 2可能会终于于x=root
  • 3->4
  • 4->修复

 

  总结,对insertdelete算法的修复算法,我们可以发现如果当前的红黑树不能直接修复时,则通过
  1. 改变x的指向和一些结点的颜色
  2. 改变二叉树的形状和一些结点的颜色来得到一个可以修复的case

  改变时应该注意保持每条路径的黑色结点数目不变(即使改变前每条路径的黑色结点不一致,也要保证改变前后每条路径黑色结点数目不变),具体怎么去改变,则要通过自己多在纸上画画图了。

posted @ 2013-04-14 17:57  FingerDancing  阅读(1894)  评论(0编辑  收藏  举报