转载地址:https://www.cnblogs.com/wskwbog/p/11236136.html
红黑树这个数据结构,让你又爱又恨?看了这篇,妥妥的征服它
红黑树是一个比较复杂的数据结构,相信很多人也只知其名而不知其意,因为理解它的原理确实需要花费一定的功夫。之所以写这篇文章,也是为了更好的理解 Java 中 TreeMap 的源码。
写之前,搜了下网上的文章,说实话,看完有点懵,大部分一上来就给你它的五大性质,然后就是一顿插入、删除、旋转操作,就完事了,理解起来相当吃力。
本文将结合 2-3-4 树,循序渐进地介绍红黑树的由来和原理,相信看完之后,你对它会有更清晰的认识。此外,这里描述的是普通红黑树,而不是它的变体左倾红黑树(LLRB),这一点需要注意。
红黑树的由来要从二叉查找树说起。二叉查找树是一颗二叉树,它每个结点的值都大于其左子树的任意结点而小于右子树的任意结点,它结合了链表插入的灵活性和有序数组查找的高效性(二分查找)。
对于使用二叉查找树的算法,它的运行时间取决于树的形状,而树的形状又取决于结点插入的先后顺序。如上图所示,最好情况下,N 个结点的树是完全平衡的,每条空链接到根结点的距离都为 ~lgN;而在最坏的情况下,搜索路径上可能有 N 个结点,退化成了链表。
所以,为了保证运行时间始终在对数级别,在动态构建二叉查找树时,希望保持其平衡性,也就是降低树的高度,使其尽可能为 ~lgN,这样就能保证所有的查找都能在 ~lgN 次比较内结束,就像二分查找那样,这样的树被称为平衡二叉查找树。
第一个自平衡二叉查找树就是AVL 树,它规定,每个结点的左右子树的高度之差不超过 1。在插入或删除结点,打破平衡后,就会通过一次或多次树旋转来重新平衡。
AVL 树是严格平衡的,适用于查找密集型应用程序,因为在频繁插入或删除结点的场景下,它花费在树旋转的代价太高。
而红黑树就是一种折中方案,它不追求完美平衡,只求部分达到平衡,从而降低在调整时树旋转次数。
说到红黑树,就不得不提 2-3-4 树,因为,红黑树可以说就是它的一种特殊实现,对它有所了解,非常有助于理解红黑树。
保持平衡,无非是为了降低树的高度,如果把二叉查找树一般化,允许一个结点保存多个值,变成多叉树,也可认为是降低了高度。
确切地说,标准二叉查找树中的结点称为2-结点(一个值两个子结点),现在引入3-结点(两个值三个子结点)和4-结点(三个值四个子结点),这样就能得到一颗 2-3-4 树(也称为 2-4 树)。
2-3-4 树是 4 阶 B 树,所有数据按排序顺序保存,所有叶子结点都在相同的深度。对于大多数编程语言,直接实现 2-3-4 树比较困难,而红黑树的实现相对要简单容易,这也是红黑树应用广泛的一部分原因。
红黑树是二叉树,所有的结点都是2-结点,所以为了能够表示3-结点和4-结点,为结点引入了颜色属性:
如上图所示,如果把红黑树的红色结点和其父结点放平,它的结构就和左边的 2-3-4 树一样。
现在,来看下红黑树的性质:
这些性质不必去背,就算记住后也绝对会忘,应该结合着 2-3-4 树理解性记忆。
另外,红黑树中的旋转和颜色翻转,就相当于 2-3-4 树中的拆分和合并,并且 2-3-4 树结点的拆分和合并,理解起来相当简单。对比分析和理解红黑树的操作,绝对让你眼前一亮。
在分析插入和删除之前,先了解下什么是树旋转。树旋转是二叉树中调整子树的一种操作,常用于调整树的局部平衡性,它包含两种方式,左旋转和右旋转。
其实旋转操作很容易理解:左旋转就是将用两个结点中的较小者作为根结点变为将较大者作为根结点,右旋转刚好于此相反,如上图所示:
红黑树的旋转其实就是为了确保和其结构相同的 2-3-4 树的一一对应关系,同时保证红黑树的有序性和平衡性。
接下来,就结合 2-4 树分析结点的插入,首先 2-4 树的插入逻辑是这样的:
2-4 树插入的都是叶子结点,红黑树插入的结点都是红色的,因为在 2-4 树中,待插入结点都认为可以插入到一个多值结点中。
这里假设待插入结点为 N,P 是 N 的父结点,G 是 N 的祖父结点,U 是 N 的叔叔结点(即父结点的兄弟结点),那么红黑树有以下几种插入情况:
这三种情况比较简单,就放在一起说明了,它们都不涉及旋转,只涉及颜色翻转,换句话说就是只是结点合并没有拆分。
情况 1 和 2,不影响红黑树的性质,不会打破平衡,直接插入即可:
情况 3,P 为红色(不是根结点),U 也是红色,两个树插入情况如下:
以 [7, 5, 9, 3] 输入序列为例,两个树构建过程如下:
情况 4,P 为红色,而 U 为黑色,此时,在 2-4 树看来这个结点就是一个 3-结点,直接插入变成 4-结点;而对于红黑树,它为了和这个 2-4 树结构保持一致,会根据不同的情况做旋转,分别有以下四种可能:
相反的:
以 [7, 5, 9, 3, 4] 输入序列为例,也就是在上图的基础上,插入 4,演示 P 为左,N 为右,树的旋转过程:
其他情况,左右互换即可,可自行尝试分析。这里给出最开始提供的红黑树和 2-3-4 树它们的动态构建过程,输入序列为 [7, 5, 9, 3, 4, 8, 10, 11, 12, 13, 14, 15],首先是 2-3-4 树的构建:
红黑树构建时会有一次根结点调整,可注意一下:
二叉查找树的结点无非是有两个子结点,有一个子结点和叶子结点三种,其中有两个子结点的 M 结点的删除逻辑是:
所以,删除任一结点的问题就简化成了:删除一个最多只有一个孩子的结点的情况,并且所有的删除操作都在叶子结点完成,只不过删除的结点不再是一开始想删除的结点,但结点的值最终是删除了,而树结构的变化与简化问题相比,并不重要。
在分析红黑树的删除之前,简单来看下 2-3-4 树的删除情况。
它类似二叉查找树的删除,实际的删除操作也是在叶子结点完成,只不过在删除的过程中涉及到结点的合并,主要有 3 种不同的情况:
上面这些情况,有一个前提就是,在遍历查找待删除结点时,必须保证路过的结点都至少有 2 个 key,不是的话就需要合并结点。这点比较难理解,在插入时,会把遍历过程中遇到的4-结点 进行拆分,相对的,在删除时,就要保证遍历的结点至少有 2 个 key,也就相当于把之前拆分的进行了合并。
以下图示演示了上述的每种可能的删除情况:
简单来说,理解 2-3-4 树删除的重点就是:
红黑树的删除也同样类似二叉查找树,不过要考虑平衡,也就是结点颜色问题,要麻烦一点。
首先声明一点,接下来说的红黑树叶子结点和二叉查找树叶子结点相同,如果要强调红黑树结点是空的叶子结点 NIL 会特殊说明,画图会使用黑色方框表示。
假设待删除结点为 M,如果有非叶子结点,称为 C,那么有两种比较简单的删除情况:
注意:M 有且仅有一个非叶子的左或右孩子结点,相当于 2-3-4 树删除的情况 1。
这两个情况,本质都是删除了一个红色结点,不影响整体平衡。以 [7, 5, 9, 3, 4] 输入序列构建的红黑树为例,演示以上两种比较简单的情况:
删除比较复杂的是 M 和 C 都是黑色的情况,此时 M 肯定是叶子节点,而 C 肯定是 NIL 结点,如果不是这样的情况将违反性质5。
一个黑色结点被删除会打破平衡,需要找一个结点填补这个空缺,假设待删除结点为 M,删除后它的位置上就变成了 NIL 结点,为了方便描述,这个结点记为 N,P 表示 N 的父结点,S 表示 N 兄弟结点,S 如果存在左右孩子,分别使用 SL 和 SR 表示,那么删除就有以下几种情况:
删除后,N 变成了根结点,也就是说删除前只有 M 这一个结点,直接删除即可。
S 是红色,那么它必有两个孩子结点,且都为黑色,而且 P 也肯定是黑色。此时,交换 P 和 S 的颜色,然后对 P 左旋转,如下:
现在,结点 N 的父结点变成了红色,兄弟结点变成了 SL,此时就可以按照情况 4、5、6继续处理。
P 是黑色,S 也是黑色,并且 S 也没有非空的孩子结点。此时,直接将 S 变成红色,那么经过 S 的路径也就少了一个黑色结点,整体上就导致经过 P 的路径比原来少了一个黑色结点,把不平衡状态从结点 N 转移到了结点 P,可以把 P 按 情况1 处理,直到遇到根结点,以此形成递归:
P 是红色,S 是黑色,并且 S 也没有非空的孩子结点。此时,只要交换 P 和 S 的颜色,正好填补了少一个黑色结点的空缺,也就是恢复了平衡的状态:
P 任意颜色,S 黑色,S 的左孩子红色,(S 有右孩子也是红色)。此时,对 S 右旋转,并交换 S 和 SL 的颜色:
其实就是把这种情况,转成了 情况 6 进行处理。
P 任意颜色,S 黑色,S 的右孩子红色,(S 有左孩子也是红色)。此时,对 P 左旋转,交换 P 和 S 的颜色,并将 SR 变成黑色:
此时恢复平衡的状态,无论 P 之前是什么颜色,N 都比之前多了一个黑色父结点。假设 P 原先是红色的,现在变成了黑色;假设原先是黑色的,现在 P 又多了一个黑色的父结点 S,所以,无论怎样,经过结点 N 路径增加了一个黑色结点。
以上 6 种情况,结点 N 都是左孩子,如果是右孩子,只需把左右对调即可。类比 2-3-4 树的删除,理解黑色结点删除后的关键就是:
最后来看下 2-3-4 树和红黑树动态删除的过程,输入序列为 [7, 5, 9, 3, 4, 8, 10, 11, 12, 13, 14, 15, 16],删除顺序是 [16, 13, 11, 7, 15, 14, 8, 4, 9, 10, 5, 3, 12],首先是 2-3-4 树的动态删除过程:
红黑树动态删除的过程:
红黑树确实比较复杂,单纯的分析性质和旋转,意义不大,而 2-3-4 树就不一样了,它的插入和删除简单多了,而红黑树的旋转和变色最终也是为了和同构的 2-3-4 树保持一致,本文就是相互结合分析,互相印证,相信会相对容易理解一点。
动图来自网站:https://www.cs.usfca.edu/~galles/visualization/Algorithms.html
它支持单步调试,有兴趣可以试一下。