红黑树:自平衡的二叉查找树(一)

前一篇博文中写道,二叉查找树是一种具有较高性能的符号表实现,在理想情况下它可以保证查找、插入和删除操作的时间复杂度在对数级别。二叉查找树可以动态地插入和删除元素,在插入和删除操作的过程中,二叉查找树的形状也会发生变化。

考虑以下问题:二叉查找树 T 的初始状态 T0 是一棵空树,现分别以两种不同的顺序 M(A, B, C, D, E, F, G) 和 N(D, B, F, A, C, E, G) 向其中插入 A-G 共 7 个相同的元素,当最后一个元素插入完成后,T 的最终形状 TM 和 TN 分别是怎样的。

bst-n图1 按 M(A, B, C, D, E, F, G) 顺序插入后生长的二叉查找树 TM

bst-m图2 按 N(D, B, F, A, C, E, G) 顺序插入后生长的二叉查找树TN

可以发现,同一组元素可以构建多种不同形状的二叉查找树,二叉查找树最终的形状与插入元素的顺序有关。二叉查找树的操作性能与树的深度相关,在 TM 中查找一个元素最多需要比较 7 次,而在 TN 中查找一个元素最多只需要比较 3 次。显然,TN 的性能优于 TM。可以预见,当元素集合的规模增大时,这个性能差距将进一步增大。TN 是一棵完全二叉树,它的查找性能可以达到 O(logN),而 TM 这种只有右非空子树的二叉树是另一个极端,它的查找性能在最坏的情况下是 O(N),甚至退化到与前一篇博文中提到的数组顺序查找性能相同了。

在实际应用中,我们希望有一种不受插入和删除顺序影响的性能稳定的二叉查找树,这种二叉查找树能在插入和删除的过程中自动调整以保持平衡性,保证任何时候查找的时间复杂度都能达到 O(logN) 的级别。

红黑树就是一种自平衡的二叉查找树。

红黑树的结点除了像二叉查找树有键和值的属性外,还有一个颜色属性,红黑树结点的颜色只能取“红”、“黑”之一。另外,为了描述方便,把某结点的颜色等同于指向该结点的链接的颜色,例如 B 的左子结点 A 是一个红结点,也可以描述为 B 与 A 之间的链接是红色的。一棵红黑树满足以下的条件:

  • 所有结点非黑即红;
  • 根结点是黑色的;
  • 红结点不互为父子;
  • 任一结点到其所有叶子结点的简单路径都包含相同数目的黑结点。

红黑树在插入和删除操作后也应满足以上的条件,上述几个条件的约束保证了红黑树在插入和删除结点之后也能保持平衡。如果把红黑树的红链接的长度计为 0,黑链接的长度计为1,那么从任意一个结点出发,到其所有的叶子结点的最短距离是相等的。下图是一棵典型的红黑树,空结点没有画出。

rbt图3 一棵典型的红黑树

当要向一棵红黑树中插入结点时,应当保证插入操作完成后仍然得到一棵满足上文提到的所有条件的红黑树。相比于普通的二叉查找树的插入操作,红黑树的插入操作更复杂,因为像二叉查找树一样向红黑树中插入新的结点,先不论是红是黑,势必破坏红黑树原有的平衡性。因此,向红黑树中插入新结点后,需要做一些调整,使二叉树重新恢复平衡。

在讨论红黑树插入操作的具体算法之前,先了解在红黑树局部执行的几个重要的变换:左旋、右旋和颜色翻转。

  • 左旋

假设红黑树的某个结点 Nx 具有红色的右子结点 Ny,Nx 的父结点记为 Np,可以将 Nx 与 Ny 两个结点连同它们中间的红链接向左旋转,Np 与 Ny 链接作为它的父结点,Nx 作为 Ny 的左子结点,而 Ny 原有的左子结点重新链接到 Nx 作为它的右子结点,同时将 Nx 的颜色设为红色,Ny 的颜色设为原来 Nx 的颜色。这个操作称为对结点 Nx 的左旋。上文中提到红黑树的红链接长度可以看作 0,由于左旋操作只是在被操作结点的右边删去了一条红链接,在其左边增加了一条红链接,因此左旋操作只改变红链接的位置,而并不会破坏树的平衡性。当需要把一个结点的右红链接调整为左红链接时,对这个结点执行左旋操作。下图展示了一个红黑树结点在执行左旋操作前后的变化。

rbt-rotate-left图4 红黑树结点的左旋,灰色结点表示该结点颜色可黑可红

  • 右旋

与左旋类似,假设红黑树的某个结点 Nx 具有红色的左子结点 Ny,Nx 的父结点记为 Np,可以将 Nx 与 Ny 两个结点连同它们中间的红链接向右旋转,Np 与 Ny 链接作为它的父结点,Nx 作为 Ny 的右子结点,而 Ny 原有的右子结点重新链接到 Nx 作为它的左子结点,同时将 Nx 的颜色设为红色,Ny 的颜色设为原来 Nx 的颜色。这个操作称为对结点 Nx 的右旋。与左旋理由的相同,右旋操作同样不会破坏树的平衡性。当需要把一个结点的左红链接调整为右红链接时,对这个结点执行右旋操作。下图展示了一个红黑树结点在执行右旋操作前后的变化。

rbt-rotate-right图5 红黑树结点的右旋,灰色结点表示该结点颜色可黑可红

红黑树结点的左旋与右旋操作互为逆操作。

  • 颜色翻转

假设一棵红黑树的某结点 N 是黑色结点,它的左右子结点均为红色结点,那么可以把它的左右结点均染成黑色,结点 N 染成红色,这个操作称为颜色翻转。这个操作在结点 N 的左右各删除了一条红链接,同时各增加了一条黑链接,而在结点 N 与它的父结点之前删除了一条黑链接,增加了一条红链接。按前文的定义,红链接的长度为 0,N 结点的所有子结点的到根结点的距离都没有变化,因此颜色翻转操作也不会影响红黑树的平衡。下图展示了一个红黑树结点在执行颜色翻转操作前后的变化。

rbt-flip-colors图6 红黑树结点的颜色翻转

用红黑树实现的符号表,同样至少应当支持查询、插入和删除等操作。由于查找操作是一个只读操作,不会改变红黑树的结构,因此在红黑树上进行查找的实现与二叉查找树完全一样,不再赘述。

向红黑树中插入一个新结点分为两个步骤,第一个步骤与二叉查找树的插入操作一样,在红黑树中查找被插入的结点的键,如果查找命中那么直接用被插入结点的值替换命中的结点。如果查找没有命中,那么在查找过程中最后一次访问的那个结点 N 是插入点,如果将要插入的结点的键小于 N 的键,那么将该结点作为 N 的左子结点,否则作为它的右子结点。

新的结点插入后,是时候讨论红黑树平衡的问题了。首先,新插入的结点应当是什么颜色的?红黑树的约束之一是“任意结点到其所有的叶子结点的简单路径都包含相同数量的黑结点”,前面讨论过,如果把红链接的长度看作 0,把黑链接的长度看作 1,这个条件还可以表达为“任意结点到其所有的叶子结点的最小路径长度都相等”。可见,如果新插入的是一个黑色结点 N,由于多出了一条黑链接,那么从根结点出发到 N 的最短路径长度一定比到其它所有叶子结点的最短路径大 1,这违反了红黑树的约束条件。而如果插入的是红结点,则完全没有这样的问题。因此,新插入的结点一律先将其设为红色

尽管可以把新插入的结点设为红色来避免出现不平衡,但有一些问题仍然没有回避,例如如果新插入的结点的父结点也是红色,则这棵树又违反了红黑树“红结点不能互为父子”的原则。当遇到这种情况时,就需要运用上文中提到对红黑树结点的几种变换来使这棵树重新符合红黑树的原则了。

为了简化问题,降低算法的复杂度,在具体实现一棵红黑树时,我参考了《Algorithms (Fourth Edition)》一书中的做法,在标准红黑树的约束条件下,再加一条约束条件:所有红链接一定均为左链接。加上了这个条件的红黑树依然能实现,并且能完成标准红黑树所有的操作。下文所有的内容所描述和实现的红黑树均是加了这个附加条件的红黑树,但请注意,具有右红链接的红黑树也是可用的,只是实现起来更加复杂。

在向一个非空红黑树插入一个新结点后,有多种不同的情况需要分别处理。为了描述方便,将新插入的结点记为 Nn,被插的结点(即新结点的父结点)记为 Np,与 Nn 拥有共同父结点的结点(称为 Nn 的兄弟结点)记为 Nb

(一)如果 Nb 和 Np 都是黑色结点(空结点也视为黑色结点,下同),那么情况比较简单:

  1. 如果 Nn 是 Np 的左子结点,此时整个树仍然满足红黑树所有的约束条件,不再作任何处理,插入操作完成;
  2. 如果 Nn 是 Np 的右子结点,Nn 与 Np 之间出现了一条右链接,不满足上文提到的红黑树附加条件,此时对 Np左旋操作,把红链接旋转为一条左链接,操作完成。

(二)如果 Nb 是红色结点,由红黑树的性质可知 Np 此时一定是黑色结点,此时 Np 的左右均为红链接,对 Np颜色翻转操作,将 Nn 与 Nb 全变为黑色,并将 Np 变为红色。由于 Np 处多了一个红结点,因此同要样要以(一)、(二)和(三)的规则再处理 Np

(三)如果 Np 是红色结点,像(一)一样,有如下两种不同的情况:

  1. 如果 Nn 是 Np 的左子结点,此时出现了两个连续的左红链接,不符合红黑树的约束条件,对 Np 的父结点 Npp(称为 Nn 的祖父结点)做一次右旋操作,把 Np 的左红链接旋转到右侧,于是 Np 左右就有了两条红链接,分别连着 Nn 和 Npp,再对 Np 做(二)中提到的操作;
  2. 如果 Nn 是 Np 的右子结点,此时对 Np 做一次左旋操作,又出现了两条连续的左红链接,再对 Np 做(三).1 中提到的操作。

(二)、(三)情况的图示如下。

图7 红黑树插入新结点后的一些调整操作

上述(二)中的情形会将红链接传递到父结点,当父结点的父结点也碰到红链接时又会将红链接向上传递,直到遇到根结点。当根结点变成红色时,需要把它重新染黑,根结点由红变黑一次意味着红黑树的高度增加了 1。因此,与二叉查找树相反,红黑树是自下向上生长的。下面的动画展示了一棵红黑树在插入一个新的节点后红链接向上传递的过程。

rbt-insert-ani图8 一棵红黑树在插入一个新结点后的生长过程

与二叉查找树一样,红黑树的删除操作比插入操作更加复杂,由于篇幅已经很长,关于红黑树的删除留到下篇博文中讨论。

本文中提到红黑树的所有算法均已用 Java 实现非递归版本,GitHub 版本库地址:查看

参考文献
[1] Sedgewick R, Wayne K. Algorithms[M]. 人民邮电出版社, 2012.
[2] 侯捷. STL 源码剖析[M]. 华中科技大学出版社, 2002.

(允许转载本文,转载请注明出处:http://luodichen.com/blog/?p=139)