红黑树解法的why而非how

0 初衷

很多介绍红黑树的文章如同算法导论书中那样,都是上来直接给出一些分类情况,以及每个分类情况下的处理办法,而没有着重讲述为什么这么分类,为什么这个分类下执行这些操作,即只介绍了how,没有重点给出why。本篇文章的重点就在于解释why。

这样可能就导致一种现象:我按照这些分类以及分类下的操作办法,的确完整的走通想通了整个算法过程,感觉应该理解红黑树了,但是可能过了几个月后就忘记如何分类的了,忘记分类下如何操作的了。

归根到底是我们没有找出最本质的东西,比如说插入节点时遇到父子是红红的问题,对应的2个直接的解决办法如下:

  • 将父节点改成黑色
  • 将一个红节点扔到另一个子树中

这2个解决办法就是直接解决当前父子红红问题的,然后就可以在这2个办法的基础上进行详细的开展,就会推出插入节点时的分类情况及其操作办法了。文章的后面会详细展开这部分的推理,下面还是先把基础的二叉搜索树介绍下。

1 二叉搜索树

1.1 定义

二叉搜索树中的每个节点含有如下属性,key、left、right、p,分别对应该节点的值、左孩子、右孩子和父节点。

并且满足如下性质:

设x是二叉搜索树的一个节点,如果y是x左子树中的一个节点,那么y.keyx.key。

如下所示:

1.2 基本操作

最大、最小值都很简单,这里不再赘述了。

1.2.1 前驱和后继

按照中序遍历的方式来给出指定节点的前驱和后继

  • 后继

    如果该节点有右子树,则后继节点就是右子树的最小值
    如果没有右子树,则后继沿着该节点往上找到一个父节点,该父节点的左子树包含当前节点。

    如何来理解呢?

    中序遍历的顺序为:左根右。那么以当前节点作为根,查找它的后继:

    如果有右子树,则 为 (左子树)根(右子树) ,很明显紧跟根后面的就是右子树中的第一个元素,按照中序遍历的方式右子树中第一个元素就是右子树的最小值

    如果没有右子树,则为 (左子树)根,该部分中序遍历结束,那么这一部分可能是上层父节点的左子树部分或者右子树部分,如果是上层父节点的右子树部分,那么上一层父节点的根在该部分的前面,即为 根1((左子树)根),此时同理,上层父节点的根也中序遍历完毕,开始上上层父节点的遍历,如果还是右子树,继续轮回,即为 根2 根1((左子树)根),

    如果是左子树,那么下一个父节点则在该部分的右面,即,根2 根1((左子树)根)根3。所以我们要找的节点的后继就是根3。

    最好在纸上进行划分下。该算法见算法导论中下图

  • 前驱

    同理

1.3 查找

也挺简单的,如下:

1.4 插入

也比较简单,就是对比找到一个节点,直到遇到NIL节点,则该NIL节点的父节点就是要插入节点的父节点

然后再判断要插入的节点是比父节点大还是小,大的话作为右节点,小的话作为左节点。

见算法导论中下图:

1.5 删除

针对要删除节点(设为z节点)的孩子,分下面3种情况:

  • 无孩子:直接删除当前节点即可,修改父节点,用NIL作为孩子替换z。
  • 只有一个孩子:修改父节点,将该孩子替换z。
  • 有2个孩子:找到z的后继(这里设置为y)来替换z。此时的后继y必然是z的右子树中的最小值。这就需要先将y从右子树中摘出来,即需要先将y的右子树替换y,摘出y后,再用y去替换z。让z的左子树作为y的左子树,z的新右子树(摘除y之后)作为y的右子树。

    总结下就是:y(一定没有左孩子)的右孩子替换y,y替换z的过程。

    此时有一个可以优化的地方就是:如果y是z的右孩子,那么z的新右子树就是y的右子树,那么此时就可以不用摘除了。

算法内容如下:

先定义一个节点替换的方法:

整体删除逻辑如下:

至此,基础的二叉搜索树就算铺垫完成了,下面就是重点的红黑树了。

2 红黑树

2.1 红黑树的5个性质

  • 1 每个节点或是红色,或是黑色
  • 2 根节点是黑色
  • 3 每个叶节点(NIL)是黑色
  • 4 如果一个节点是红色,则它的2个子节点都是黑色
  • 5 对于每个节点,从该节点到其所有后代叶节点的简单路径上,均包含相同数目的黑色节点

一颗红黑树示例如下:

可以得出的一些结论:

  • 红黑树的任何一个节点的左右子树的高度最多是另一子树的2倍。

    由性质5和性质4可以得出,最短的路径都是黑色节点,最长的路径有交替的红色和黑色节点,所以一个子树最多是另一个子树高的的2倍,就是所谓的近似平衡。

2.2 旋转

可以参见下这里的一个动画浅谈算法和数据结构: 九 平衡查找树之红黑树

旋转的作用:

  • 用来平衡左右子树的高度,而不违反二叉搜索树的性质,简单可以理解为**将一个节点扔到另一个子树中**

2.3 插入

2.3.1 普通插入

按照上述二叉搜索树的插入方式

2.3.2 插入后的修正

新插入的节点着色为红色,暂叫z节点,z的父节点叫z.p,它会违反红黑树的哪些性质?

  • 违反性质2

    如果插入节点是根节点,则违反了性质2。此时只需要将z节点重新着为黑色即可。

  • 违反性质4

    如果z.p是红色,那么就违反了性质4。下面就针对性质4来具体的分析怎么解决

目前要解决的问题是:z是红色,z.p也是红色

可以得到的一些结论:

  • z.p.p必然是黑色

    因为在z插入之前是一颗红黑树,必然要满足性质4,所以z.p.p是黑色,z的叔父节点是不确定的,可红可黑。

  • z的兄弟节点只能是叶节点(黑色的)

    z.p是红色的,则z的兄弟节点必须是黑色的,如果z的兄弟节点不是叶节点,则z的兄弟节点必然比z这一路多了一个黑色,不满足性质5,所以z的兄弟节点是黑色的叶节点。

解决问题的要领:

不能增加或者减少黑高,只能部分增加并且部分减少然后相互抵消

解决父子红红问题有2个思路:

  • 1 把z.p变成黑色,z.p.p变成红色
  • 2 把z和z.p中的一个红色节点扔到z.p.p的另一个子树中

再来详细分析下这2个思路:

  • 1 把z.p由红色变成黑色,z.p.p由黑色变成红色

    z这一路增加一个黑色,减少一个黑色,黑高不变。但是z的叔父那一路就会因为z.p.p而少了一个黑色,所以如果z的叔父是红色,那么可以将z的叔父由红色变成黑色来抵消z.p.p的减少。

    所以这个就要求z的叔父节点是红色。

    但是这并没有完,我们将z.p.p由黑色变成红色,可能又会出现父子红红的情况,所以继续下一次的轮回

    初始为:

    转变成如下:

  • 2 把z和z.p中的一个红色扔到z.p.p的另一个子树中

    这个扔的操作就是通过左旋或者右旋,目前假如是右旋,即将z.p提升为z.p.p的位置,将z.p.p拉下来作为z的叔父节点的一路。颜色上,z.p由红变成黑色,z.p.p由黑色变成红色(这就要求z的叔父必须是黑色,不然又出现红红)。此时z和z的叔父2路都没有增加或者减少黑色。同时z.p的右子树会作为z.p.p的左子树(右旋的结果),此时z.p.p是红色,则z.p的右子树必须是黑色,即z必须是z.p的左子树。

    一种情况z本来就是z.p的左孩子,另一种情况z是z.p的右孩子,那么可以通过左旋就可以实现,然后减交换z和z.p的位置。所以这里又分2种情况。分析完毕,下面看图形示意。

    初始为:

    这里需要将4或者5中的一个红色扔到6的另一个子树中,那么执行右旋后如下:

    这里其实就要求5节点必须是黑色,即在旋转前4节点的右孩子必须是黑色,那么z必须是4节点的左孩子。这里我们只需要执行下左旋,将可以将红色的子转变成红色的父的左孩子,此时再将z节点设置为红色的子即4节点即可,执行上述的操作了。

总上2点解决方案的分析,我们可以得出如下的3个分类情况以及解决办法:

  • 1 z的叔节点y是红色

    即按照上述思路1中的方式,把z.p由红色变成黑色,z.p.p由黑色变成红色,z这一路保持黑高不变,z的叔节点由红色变成黑色,同样保持了黑高不变。z.p.p重新定义为z,继续下一次的轮回。

  • 2 z的叔节点y是黑色的且z是一个右孩子

    即按照上述思路2中的方式,通过左旋,来保证z.p的右子树不能是红色,必须是黑色(因为这个右子树将来要作为红色节点的孩子节点)

  • 3 z的叔节点y是黑色的且z是一个左孩子

    即按照上述思路2中的方式,通过对z.p进行右旋,z.p顶替了z.p.p及其颜色,z.p.p拉下着为红色,并将z.p之前的右子树挂到z.p.p的左子树下。

2.4 删除

2.4.1 普通删除

再来简单回顾下二叉搜索树的删除:

要删除的节点为z

删除就是分如下的3个条件:

  • z没有孩子:直接删除
  • z只有1个孩子:将孩子替换z
  • z有2个孩子:用z的后继y来替换z,同时y的右孩子来替换y的过程。

2.4.2 修正

这时候可能有3个节点:

  • z:要删除的节点
  • y:z的后继
  • r:y的右孩子

在没删除之前,要删除的节点z的颜色可红可黑,y可红可黑,r如果存在的话只能是红色(因为y是没有左孩子的,如果r存在并且是黑色的,那么r这一路包含的黑色节点个数比y的另一路多,不符合性质5)。

问题就来了:这里有几个位置的节点需要进行修复?z的位置?y的位置?r的位置?

结合上面删除的3种情况来详细分析下:

  • z没有孩子:直接删除。

    如果z是红色的话,此时什么性质都没有违反。

    如果z是黑色的话,那么就少了一个黑色,之后需要补一个黑色。

  • z只有一个孩子:直接将孩子替换成z。

    如果z是红色,则z的孩子必然都是黑色,由于只有1个孩子,那么就违反了性质5,则这种情况是不存在的。

    那么z只能是黑色,并且那一个孩子只能是红色。此时孩子替换了z,缺少了一个黑色,需要补回来。

  • z有2个孩子:y的右孩子替换y(如果有的话),y替换z

    那么z的后继y要替换z,继承z的颜色,那么z处就仍保持不变。y的右孩子r(如果有的话)要替换y,那么此时可能违反性质的地方就是y处了。

    如果y是红色,则孩子必须是黑色,y是z的后继,则y是没有左孩子,y如果有右孩子,则必然是黑色,此时又不满足性质5。所以y是红色的时候,y是没有右孩子的。那么此时删除y就不违反任何性质。

    如果y是黑色,如果有右孩子,则必然是红色(如果是黑色则不满足性质5)。所以不管有没有右孩子,此时删除y都是少了个黑色的,需要补回来的。

从上面可以看到有时候是需要修复z处的,有时候是需要修复y处的。进行统一概括的话,设置一个变量为x节点,该节点为需要修复的地方,得到如下结论:

  • 如果x处节点原本是红色的话,没有违反任何性质,不需要修复
  • 如果x处节点原本是黑色的话,都是少了一个黑色,需要修复的

其中x在前2种情况下就是原z处节点,在最后一种情况下就是原y处节点。

下面要解决的问题就是:对x节点缺少一个黑色的修复

最直接的解决办法如下:

  • 1 对x这一路多补充一个黑色节点,即执行一次左旋或者右旋,往x这一路多扔一个节点,然后将该节点着为黑色
  • 2 对x的父节点加一层黑色

下面就来详细展开下上述2个解决办法,来推导出对应的情况:

  • 1 对x这一路多补充一个黑色节点,即执行一次左旋或者右旋,往x这一路多扔一个节点,然后将该节点着为黑色

    这时候相当于x的兄弟节点那一路要给出一个节点,即它就少了一个节点。少了一个节点还要能保住黑高不变,则必然是扔了一个红色节点过来。扔过来后,再把该节点着为黑色,就为x这一路多增加了一个黑色,从而弥补了缺少的黑色。

    设定x的兄弟节点是w。上述扔的操作可能执行的是左旋也可能是右旋,这里先假定是左旋。则这里w的右孩子替代w,w替代w的父节点及其颜色,w的父节点拉下来,并着为黑色。

    根据上述描述,要想保证w这一路在少了一个节点之后,黑色数目不变,则w和w的右孩子必然有一个是红色,一个是黑色(2红违反性质4所以不可能,2黑则必然要少一个黑色所以也不可能)。到底谁是红色呢?

    w的左孩子要作为w的原来父节点(该节点要被着为黑色)的右孩子,所以w的左孩子要挂在黑色的父节点下,要想保证黑高不变,那么该节点之前挂在w节点下,那么w节点也必须是黑色,则w的右孩子是红色了。

    下面来看下这个过程:

    原本为:

    经过上述左旋并着色的操作过程后为:

    所以只要满足了w为黑色,w的右孩子为红色,就可以采用该解决办法

    为了满足w为黑色,w的右孩子为红色。也分如下几种情况:

    • w为红色,则w的孩子必然为黑色(你可能认为万一w没有孩子怎么办?这里是不可能的,因为w的另一路z缺少了一个黑色,那么之前必然有一个黑色,而如果w没有孩子,则w这一路必然比z那一路少一个黑色,不满足性质5,所以不存在这种可能)。这时候如果对w进行左旋或者是右旋必然会改变左右黑高的不平衡。w为红色,则w.p为黑色,此时可以把w这一路的红色扔过去即执行左旋,w.p变为红色,w变为黑色。w的左子树(左孩子为黑色)作为w.p的右孩子。重新更正w为原w的左孩子,由于颜色为黑色,则可转到如下几种情况:
    • w为黑色,w的右孩子为红色,这种本身就满足
    • w为黑色,w的右孩子为黑色,左孩子为黑色:这种不存在红色,排除
    • w为黑色,w的右孩子为黑色,左孩子为红色:可以进行右旋,如下图所示的转换

      原本为:

      右旋并着色后:

  • 2 对x的父节点加一个黑色

    父节点加一个黑色,那么就要求x的兄弟节点必须要减少一个黑色。从上述解决办法1种可以得到,只有在w为黑色,w的2个孩子都为黑色的时候,无法采用解决办法1。这时候可以来采用解决办法2。

    将w着为红色,那么w这一路就可以减少一个黑色。

    减少了一个黑色之后,如果x的父节点是红色,那么直接将红色着为黑色,即可达到父节点多加一个黑色。如果x的父节点是黑色,那么就意味着x的父节点无法增加一个黑色,即缺少一个黑色,即将x设置为x的父节点,重新进入下一个轮回。

    你可能意识到,如果一直轮回直到x的父节点是根,此时仍无法添加一个黑色,那么此时相当于全部所有节点都减少了一个黑色,仍然是不违反任何性质的,只是比之前的黑高小1。

所以从上面的2个解决办法中可以总结出来如下算法导论中的4种情况:

  • 1 x的兄弟节点w是红色:

    可以通过左旋来重新更正w的位置,更正后的w为黑色,转为如下几种情况

  • 2 x的兄弟节点w是黑色,w的孩子全部是黑色

    利用解决办法2,将x的父节点加一个黑色,将w这一路减少一个黑色。如果父节点是红色,那么直接着为黑色即相当于加上了黑色,如果父节点是黑色,那么将x设置为x的父节点,认为此时x仍然是缺少一个黑色,即开始下一个轮回

  • 3 x的兄弟节点w是黑色,w的右孩子是黑色,左孩子是红色:

    可以通过右旋达到下面的情况4

  • 4 x的兄弟节点w是黑色,w的右孩子是红色

    可以利用解决办法1,将w那一路的红色节点扔到x这一路,然后着为黑色,即x增加了一个黑色。而w那一路减少的是一个红色,所以黑高仍然保持不变。

至此,我们来总结下删除的2个重要过程:

  • 1 确定要修复的x的位置

    x的位置可能是z处,也可能是y处。参见算法导论中这一个过程:

    上述RB-TRANSPLANT函数就是节点替换的函数,RB-DELETE-FIXUP函数就是修正函数

  • 2 修正过程

    修正过程即按照上述的分析总结对应的4种情况来处理,见算法导论如下图

至此,总算是将红黑树主要的插入和删除描述完毕了。

3 红黑树总结

过程各种分析情况挺多的,但是我们更应该去记住本质的东西,从而能够达到自己去推理对应的过程,所以再次强调

  • 插入:

    为了解决父子红红问题,有如下2种直接的解决办法:

    • 父节点设置为黑色
    • 一个红节点扔到另一个子树中
  • 删除:

    为了解决缺少一个黑色的问题,有如下2种直接的解决办法:

    • 增加一个黑色节点,可以通过左旋来得到,即另一子树扔过来一个红色(本身保证黑色不减少)
    • 父节点增加一个黑色

我们从上述直接的解决办法中就可以推理出去,自然就能得到该怎么分类,每个分类下具体怎么操作了。

一个可以检验你是否真的理解红黑树的办法就是:不借助任何东西,你自己是否能够在一张白纸上自己推导一遍。

本文章涉及到细节很多,难免有疏漏的地方,还请批评指正。

欢迎继续来讨论,越辩越清晰

欢迎关注微信公众号:乒乓狂魔

时间: 2024-10-28 17:17:04

红黑树解法的why而非how的相关文章

数据结构和算法05 之红-黑树(看完包懂~)

(友情提示,红-黑树是基于二叉搜索树的,如果对二叉搜索树不了解,可以先看看:二叉搜索树 )                从第4节的分析中可以看出,二叉搜索树是个很好的数据结构,可以快速地找到一个给定关键字的数据项,并且可以快速地插入和删除数据项.但是二叉搜索树有个很麻烦的问题,如果树中插入的是随机数据,则执行效果很好,但如果插入的是有序或者逆序的数据,那么二叉搜索树的执行速度就变得很慢.因为当插入数值有序时,二叉树就是非平衡的了,排在一条线上,其实就变成了一个链表--它的快速查找.插入和删除指

java集合类TreeMap和TreeSet及红黑树

看这篇博客前,我觉得很有必要先看下我之前的几篇博客 Red-Black Trees(红黑树)                                         (TreeMap底层的实现就是用的红黑树数据结构) 探索equals()和hashCode()方法                                 (TreeMap/TreeSet实现使用到的核心方法) java中的HashTable,HashMap和HashSet      (同为java集合类,对比下他们

java中treemap和treeset实现(红黑树)

TreeMap 的实现就是红黑树数据结构,也就说是一棵自平衡的排序二叉树,这样就可以保证当需要快速检索指定节点. TreeSet 和 TreeMap 的关系 为了让大家了解 TreeMap 和 TreeSet 之间的关系,下面先看 TreeSet 类的部分源代码: public class TreeSet<E> extends AbstractSet<E> implements NavigableSet<E>, Cloneable, java.io.Serializab

算法之红黑树

红黑树(一) 原理和算法详细介   1 R-B Tree简介     R-B Tree,全称是Red-Black Tree,又称为"红黑树",它一种特殊的二叉查找树.红黑树的每个节点上都有存储位表示节点的颜色,可以是红(Red)或黑(Black). 红黑树的特性:(1)每个节点或者是黑色,或者是红色.(2)根节点是黑色.(3)每个叶子节点(NIL)是黑色. [注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!](4)如果一个节点是红色的,则它的子节点必须是黑色的.(5)从一个

红黑树-插入删除

操作 因为每一个红黑树也是一个特化的二叉查找树,因此红黑树上的只读操作与普通二叉查找树上的只读操作相同.然而,在红黑树上进行插入操作和删除操作会导致不再符合红黑树的性质.恢复红黑树的属性需要少量(O(log n))的颜色变更(实际是非常快速的)和不超过三次树旋转(对于插入操作是两次).虽然插入和删除很复杂,但操作时间仍可以保持为 O(log n) 次. 插入 我们首先以二叉查找树的方法增加节点并标记它为红色.(如果设为黑色,就会导致根到叶子的路径上有一条路上,多一个额外的黑节点,这个是很难调整的

第十三章 红黑树

R-B Tree简介     R-B Tree,全称是Red-Black Tree,又称为"红黑树",它一种特殊的二叉查找树.红黑树的每个节点上都有存储位表示节点的颜色,可以是红(Red)或黑(Black). 红黑树的特性:(1)每个节点或者是黑色,或者是红色.(2)根节点是黑色.(3)每个叶子节点(NIL)是黑色. [注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!](4)如果一个节点是红色的,则它的子节点必须是黑色的.(5)从一个节点到该节点的子孙节点的所有路径上包含相

第十四章 红黑树——C++代码实现

红黑树的介绍 红黑树(Red-Black Tree,简称R-B Tree),它一种特殊的二叉查找树.红黑树是特殊的二叉查找树,意味着它满足二叉查找树的特征:任意一个节点所包含的键值,大于等于左孩子的键值,小于等于右孩子的键值.除了具备该特性之外,红黑树还包括许多额外的信息. 红黑树的每个节点上都有存储位表示节点的颜色,颜色是红(Red)或黑(Black).红黑树的特性:(1) 每个节点或者是黑色,或者是红色.(2) 根节点是黑色.(3) 每个叶子节点是黑色. [注意:这里叶子节点,是指为空的叶子

红黑树的实现源码

最近因为要给ccache加入红黑树的支持, 找出来曾经实现的代码作为参考, 这才发现原来 的实现都是有问题的,也怪我的测试用例写的不好, 仅仅对插入操作进行了测试, 我向所有因 为阅读了这份代码而造成困惑的朋友表示道歉. 这次重新实现, 所有的代码推倒重新编写, 参考了linux内核中红黑树的实现算法, 并且 对测试用例进行了加强,希望这是最后一个对红黑树算法的修订版本. /*-----------------------------------------------------------

浅谈算法和数据结构 九 平衡查找树之红黑树

前面一篇文章介绍了2-3查找树,可以看到,2-3查找树能保证在插入元素之后能保持树的平衡状态,最坏情况下即所有的子节点都是2-node,树的高度为lgN,从而保证了最坏情况下的时间复杂度.但是2-3树实现起来比较复杂,本文介绍一种简单实现2-3树的数据结构,即红黑树(Red-Black Tree) 定义 红黑树的主要是像是对2-3查找树进行编码,尤其是对2-3查找树中的3-nodes节点添加额外的信息.红黑树中将节点之间的链接分为两种不同类型,红色链接,他用来链接两个2-nodes节点来表示一个