《算法导论》学习笔记(4)——红黑树

         红黑树是一棵二叉搜索树,它在每个结点上增加一个存储位来表示结点的颜色(红或黑)。关于二叉搜索树的内容,可参见本系列之前一篇博客:

《算法导论》学习笔记(3)——二叉搜索树

         红黑树的每个结点都有如下五个属性:color、key、left、right、parent

 

         红黑树必须满足如下5个性质:

         ①每个结点的颜色或是红色,或是黑色

         ②根结点是黑色

         ③每个叶结点是黑色的

         ④如果一个结点是红色的,则它的两个子结点都是黑色的

         ⑤对每个结点,从该结点到其后代叶结点的简单路径上,均包含相同数目的黑色结点。

 

         对于红黑树的操作,主要是插入和删除结点。很容易想到,当插入或删除一个结点时,会破坏上述五个性质,所以我们需要对红黑树进行维护。就像二叉堆一样,每次操作之后需要维护,使得重新恢复其性质。在红黑树中,维护的方法是旋转操作。当然,不是简单一次旋转就能恢复其性质,还要根据具体情况作出判断,旋转哪个结点,左旋还是右旋。旋转只是一个基本的操作步骤。

        

旋转( rotation )

先考虑左旋( left_rotate ):

         对一个结点x进行左旋,要求x必须要有右子结点。

过程:假设要左旋的结点是x,x的右子结点是y。左旋过后,使得:

         y的父结点从x变为x的父结点,x变为y的右子结点,y的左子结点变为x的右子结点。


插入( rb_insert )

         简而言之就是先将要插入的结点设为红色,然后用类似搜索二叉树的Tree_insert()方法rb_insert(),根据结点key的大小插入到合适的位置,然后再实现一个rb_insertFixup()方法来维护红黑树,使其恢复性质。

         rb_insert()与Tree_insert()有4处不同:

         ①Tree_insert中的NULL被红黑树中的T.nil结点替代,后者是代替红黑树中所有NULL结点的结点,包括根结点的父结点、所有的叶子结点。

         ②将插入的新结点(此时为叶子结点)的左右都指向T.nil

         ③将插入的新结点设为红色

         ④最后调用rb_insertFixup()方法来维护当前的红黑树。

 

         这里解释一下为什么插入的结点设为红色。其实并不是因为违反了性质⑤才不将其设为黑色。如果是这样的话,选择红色也违反了性质②或④,而且即使违反了性质也是可以调整的,就像rb_insertFixup()一样。其真正原因是插入节点共有六种情况(通过rb_insertFixup中的判断语句可以看出:插入节点z的父节点为左孩子有三种,为右孩子也有三种。举父节点为左孩子为例:第一种为如果z的叔结点为红是第一种情况,如果叔结点为黑且z为右孩子为第二种情况,如果叔结点为黑且z为左孩子为第三种情况),前三种情况中只有第二种z最终为黑色节点,其余两种z最终为红色。z最终为红色的比例较高,(4:2),所以一开始将z定义为红色,最后不用修改的概率较高,这是考虑到代码的优化而不是因为违反了性质⑤。

 

插入的维护( rb_insertFixup ):

         首先,我们可以知道,插入一个结点,只会破坏性质②和④。我们对此进行分类:

一、破坏性质②:只有当红黑树为空时,插入的结点成了根结点。而根结点要求是黑色的,我们只需简单地将根结点从黑色涂成红色。

二、破坏性质④:由于插入的结点总是在最底层,所以我们需要自底向上地修改红黑树,“当前结点”从插入的结点开始,逐步向上维护,直到根结点。维护的方法有两种:一是改变结点的颜色,二是对结点进行旋转操作。我们可以看到在这个过程中,性质的冲突都是发生在“当前结点”上,所以我们也就将注意力集中与此。

         1.当前结点的叔结点是红色。

         需要注意的是,我们的维护是自底向上的,所以对当前要维护的结点的上面(更准确的说法是层数小于当前结点层数的结点),都是满足红黑树的性质的,所以我们一旦知道当前结点是叔结点是红色,也就知道了当前结点的祖父结点是黑色,父结点是红色(父结点和本身都是红色才会违背性质④)。我们只需要将父、叔结点涂成黑色,祖父结点涂成红色,而本身颜色不需要改变。

         这样一来只是维护好了自己的“小家庭”,包括父、叔结点和祖父节点。而且保证了性质⑤:红变黑、黑变红,并不会增减某一条路径中黑结点的个数。至于祖父节点的变色可能会导致它和它父亲结点的冲突,所以我们就将“当前结点”这个“不合群”的帽子扣在了祖父结点,让它继续地被维护。

         2.当前结点是右孩子,且其叔结点是黑色。

         将当前父结点做为新的当前结点,将新的当前结点做左旋。

         3.当前结点是左孩子,且其叔结点是黑色。

         将当前父结点做为新的当前结点,将新的当前结点涂成黑色(之前是红色),对新的当前结点的父结点做右旋。

         很容易发现,情况3和情况2其实是相通的:只要将情况2的父结点做一次左旋,就得到了情况3。


删除( rb_delete )

         与插入类似,删除操作也是有这样几个步骤:

         首先,按照搜索二叉树的删除操作删除要删的结点,然后针对每种情况做换色和旋转操作,使其恢复红黑树的性质。

         回忆一下二叉搜索树的删除操作,对于待删结点来说,分为以下三种情况:

         ①没有孩子,即叶子结点,直接使其父结点指向NULL,并释放该结点。

         ②只有一个孩子,就把其父结点的指针指向独生子,并释放该结点。

         ③有两个孩子,找出其左孩子中的最大结点或者右孩子中的最小结点,代替其父结点。

         所以我们对红黑树的删除也是遵循这样的操作,在删除结点之后,我们还需要多一步维护的工作rb_deleteFixup()。

         维护工作分为4种情况考虑:

         1.待删结点的兄弟结点w是红色。

         由于兄弟结点是红色,所以它一定有两个黑色的子结点。我们只需改变待删的兄弟结点和父结点的颜色,然后再对父结点做一次左旋操作,所以待删结点的兄弟结点(下图的C结点)一定是黑色。这样将情况1转为情况2、3、4之一去处理。


         2. 待删结点的兄弟结点w是黑色,且w的两个子结点都是黑色

         由于这种情况下B左边黑色高度比右边的少1,所以只需将w变成红色就满足性质⑤。


         3. 待删结点的兄弟结点w是黑色,且w的左孩子是红色,右孩子是黑色

         我们需要交换w和其左孩子的颜色,即C、D颜色互换。然后对w进行右旋,使得待删结点x的新兄弟结点new w是一个有红色右孩子的黑色结点,这样将情况3转为情况4去处理。


         4. 待删结点的兄弟结点w是黑色,且w的右孩子是红色

         此时我们可以将兄弟结点D染成当前父结点B的颜色,把当前父结点B的颜色染成黑色,兄弟结点的右子结点E染成黑色。然后对当前父结点B做一次左旋即可。


         至此,红黑树的所有操作方法都已经介绍完毕。总结一下,对于红黑树的操作,其基本是对二叉搜索树的操作。并在此基础上,时时刻刻注意红黑树的五个性质,就操作后的不同状态来恢复其性质。至于操作后的每种状态为什么要那些处理方法,就我现在的水平还未能完全理解,只是知道过程,是知其然而不知其所以然。

         具体实现代码见后两篇博客:

《算法导论》读书笔记(4)——红黑树(c语言实现)

《算法导论》读书笔记(4)——红黑树(c++语言实现)


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值