数据结构与算法 - 红黑树:插入操作的分析

在上一篇文章<红黑树:开篇>中,我们了解了红黑树的性质,约定了一些术语,以及定义了一些辅助函数。在本文中,我们要分析红黑树在插入一个结点的所有可能性,以及插入结点后,要经过哪些变化才能让红黑树重新回到平衡状态。

直接放结论,红黑树平衡的复杂处主要出现在父结点是红色,叔结点不是红色的这种情况。所有的场景,以及对应的平衡操作如下图。

红黑树的插入

红黑树系列文章目录

红黑树:开篇

红黑树:插入操作的分析(本文)

查找

插入操作包括两步:先查找插入的位置,插入结点后再重新平衡树。我们只需要先找到插入位置,然后将其父结点返回即可,和普通平衡二叉树的查找步骤类似,直接看下图:

查找位置

找到插入点的位置后,将插入结点挂载到正确位置就可以了,而结点有红色和黑色,新插入的结点需要什么颜色呢?如果为了最小代价,应该插入一个红色的结点。

如下图所示,根据性质 5(任意结点到每个子结点的路径都包含数量相同的黑结点),一个红色的新结点,会有更小的插入(平衡)代价:

插入

根据树的结点数量不同,会有很多种不同的插入情况,我们需要一一分析,找出每种情况需要哪些操作才能重新让红黑树回到平衡状态。

从上面查找结点位置的路径图,就可以看出两种插入场景:

  1. 根结点为空:直接把新的结点作为根结点,并根据性质2,将结点涂黑(这是唯一一种新结点为黑色的情况)

  2. 插入结点已存在:更新当前结点的值,不做其他改动

插入场景肯定不止这些,我们可以根据新结点的颜色、红黑树的特性,一步步推导出所有的插入场景。

一个新的红色结点不会影响红黑树的前三种特性,我们看性质4(每个红色结点的两个子结点一定都是黑色)。由于新结点是红色的,如果父结点也是红色的,那么这样就导致性质 4 失效。那么父结点的颜色不同,也会导致出现不同的插入场景。

那么如果父结点是黑色,性质 4 不会失效,性质 5 也依旧成立,我们可以直接插入,不做任何改动:

  1. 如果父结点是黑色结点:直接插入,不做其他改动

我们已经把条件缩小到父结点为红色结点,继续分析。如果父结点为红色结点,那么根据性质 4,插入结点的兄弟结点也是黑结点。插入新的红色结点后,会导致红色父结点违反了性质 4,只需要

  1. 父结点为红色:将父结点的颜色改为黑色(未完)

就又重新符合了红黑树的性质。但是把父结点颜色变为黑色后,就会导致新的问题:

祖父失衡

如上图所示,父结点变色,会导致祖父结点这条子树不符合红黑树的性质 5(任意一结点到每个子结点的路径都包含数量相同的黑结点)。要重新让树恢复平衡,只能做一件事,让叔结点路径的黑结点数量+1。既然关系到叔结点路径,那么就需要先判断叔结点的状态,再对阵下药。

前面父结点做操作,只判断了父结点的颜色,默认规定了父结点是存在的(如果父结点不存在,那么新结点的位置就是根结点)。而叔结点则不一定存在,那么就有 3 种情景:

  • 叔结点是红色的;
  • 叔结点是黑色的;
  • 叔结点不存在(空结点 Nil);

其中第二种和第三种其实是一样的,毕竟空结点也有颜色(黑色)。

叔结点是红色的

我们先分析叔结点是红色的这一种情况:

我们的问题:叔路径比父路径的黑色结点少 1,那么我们直接把叔结点涂黑,就解决了这个问题。

4.1. 父结点为红色,叔结点也为红色:将父结点和叔结点的颜色改为黑色(未完)

涂色后的情况变成了下图:

这样,我们就做到了让叔结点路径的黑结点数量+1,但是出现了一个问题,祖父结点这条子树的任一路径上的黑结点数量都+1,可能会导致祖父结点以上的树出现不符合性质 5 的情况,那我们直接将祖父结点涂为红色。

4.1. 父结点为红色,叔结点也为红色:将父、叔结点颜色改为黑色,祖父结点颜色改为红色(未完)

以上操作,做到了:

  1. 插入新结点;
  2. 保证自父结点出发的路径上,黑色结点数量不变;
  3. 保证父路径和叔路径上的黑色结点数量相同;
  4. 保证祖父路径上的黑色结点数量,与插入结点前的数量相同。

但是: 祖父结点由黑变红,就会导致一个问题,以祖父结点为基础,再继续向上的树是否还是平衡的?

我们可以参考下图,了解在整个变化过程中,当前结点和祖父结点的状态变化。

对比祖父结点

看新结点:新结点的插入,将原本的一个黑色结点,替换成了一个红色结点,且这个红色结点的两个子路径拥有相同的黑结点数量,而且在结点替换前后,以结点之下的所有路径,黑色结点的数量保持不变。

看祖父结点:祖父结点的变化就好像是我们在祖父结点的位置,“插入”了一个新的结点,用来替换祖父。那么对于祖父结点向上的树,可以递归的进行变色处理,直到整棵树重新平衡。

那么我们终于可以结束这条 4.1 分支了。把这条分支的所有操作都列出来,和前面的几个分支放在一起:

  • 1 根结点为空:直接把新的结点作为根结点,且涂黑;

  • 2 插入结点已存在:更新当前结点的值,不做其他改动;

  • 3 如果父结点是黑色结点:直接插入,不做其他改动;

  • 4.1 如果父结点是红色结点,且叔结点也是红色结点:把父、叔结点都涂黑,祖父结点涂红,然后以祖父结点为当前结点,重新进行平衡操作;

  • 4.2 如果父结点是红色结点,当叔结点是黑色结点:(未推导)

叔结点为黑色

性质4:每个红色结点的两个子结点一定都是黑色(一个结点如果包含一个红色子结点,那么它一定不是红色)

性质5:任意一结点到每个子结点的路径都包含数量相同的黑结点

如果叔结点是红色的,我们可以直接把叔结点变成黑色,做到父、叔路径的黑色结点数重新相等。但是叔结点如果不存在或为黑色,就不能直接这么做,需要另外的方法。

黑色的叔结点

如上图所示,我们插入新结点后(状态2),为了保持父结点符合性质4,需要进行涂色(状态3)。对比涂色前后的状态,就好像是父结点和祖父结点的颜色互换,这导致最终结果不符合性质 5。

如果要符合性质 5,又必须把这两个结点的颜色换回来,这自然是让我们前面的努力白费。不过有句话说的好:“山不过来,我就过去”。我们直接把父结点和祖父结点的位置互换,这样结点颜色就符合性质 5 的要求。

要做到结点位置互换,且依旧保证这棵树满足平衡二叉树的要求,那么就只有一类操作:“旋转”。

平衡二叉树中的旋转,分为左旋和右旋。旋转可以调整结点与子结点的位置关系。

旋转

从上图可以看到,某个结点的旋转,会影响到其子代结点和孙代结点的位置。(图中X点左旋,影响了子代Y以及孙代B)

那么回到我们的问题,我们要调整父结点和祖父结点的位置,那就需要让祖父结点进行左旋/右旋,但是具体要哪个操作,需要根据他们之间的位置,做不同的旋转。

父结点是左子结点

4.2 父结点是红色、且叔结点为黑色或空结点、且父结点是左子结点:父结点涂黑,祖父结点涂红(未完)

父结点是左子结点,那么想要互换位置,则需要祖父结点进行右旋,但是问题来了,右旋可以将左子结点提上来。但是,左子结点的右子树会被挂在原结点的左子树上,这会不会影响到原先的颜色平衡呢?

父结点是左子结点

按照上图的情况所示,当父结点是左子结点时,如果插入结点也是左子结点(A),那么整棵树重回平衡。但如果插入结点是右子结点时,祖父结点又出了问题。不过我们先归纳下前面那种情况:

父结点是左子结点(一)

4.2.1 父结点是红色,叔结点是黑色,父结点是左子结点,插入结点是左子结点:将父结点涂黑,祖父结点涂红,然后将祖父结点右旋。

现在我们探讨第二种情况:插入结点是右子结点。

结点颜色情况如下:父结点是黑色,插入结点为右红结点,兄弟结点为左黑结点(空结点)。如果我们能将父结点的两个子结点颜色互换,那就可以回到 4.2.1 的场景,如下图

父结点是左子结点(二)

前面我们将祖父结点右旋,做到了祖父结点和父结点的颜色互换,那么这里我们或许也可以使用旋转来做到。兄弟结点是不固定的,那么我们就单纯针对父结点和插入结点。

插入结点作为右子结点,那么就需要父结点进行左旋,来互换他们的位置,然后再遵守性质4,将颜色变为平衡的颜色。这样,4.2.2 的场景就变成了 4.2.1 的场景,那么只要将 4.2.1 的步骤再执行一次,就可以将红黑树恢复平衡。如下图

父结点是左子结点(三)

4.2.2 父结点是红色,叔结点是黑色,父结点是左子结点,插入结点是右子结点:将父结点涂黑,祖父结点涂红,将父结点左旋,父结点涂红,插入结点涂黑,将祖父结点右旋。

这里的操作步骤中,父结点涂黑又涂红,插入结点设置成红色后又涂黑,我们仔细观察 4.2.2 场景的不同状态,可以将操作减少。

父结点是左子结点(四)

将简略版的 4.2.2 步骤写下来,那就是这样的:

4.2.2 父结点是红色,叔结点是黑色,父结点是左子结点,插入结点是右子结点:父结点左旋,将父结点设置为当前结点,切换到 4.2.1 场景的操作

终于,我们把父结点是左子结点的场景也罗列出来,并给出平衡方案。那么如果父结点是右子结点呢?左子结点和右子结点作为对称的两个结点,只要把前者的操作进行左右镜像后,就变成了后者的操作步骤,这里可以自行推导。

总结

以上,就是红黑树插入结点后的平衡步骤分析,将其绘制成一个思维导图

红黑树的插入


参考文章

Java 全栈知识体系 树 - 红黑树(R-B Tree)(by pdai)

30张图带你彻底理解红黑树(by 安卓大叔)

红黑树(一)之 原理和算法详细介绍(by skywang12345)

posted @ 2021-11-30 08:22  Asjun  阅读(118)  评论(0编辑  收藏  举报