讲红黑树之前,我们先来回顾一下 二叉查找树 (BST) ,先来看一下二叉查找树的特性:
- 左子树上所有结点的值均小于或等于它的根结点的值。
- 右子树上所有结点的值均大于或等于它的根结点的值。
- 左、右子树也分别为二叉排序树。
我们来看一个典型的二叉查找树:
这样的数据结构有什么好处呢?首先我们来测试一下,试着查找一下值为 10 的节点。
1. 查看根节点 9 。
2.由于10 > 9,因此查看右孩子13。
3.由于10 < 13,因此查看左孩子11。
4.由于10 < 11,因此查看左孩子10,发现10正是要查找的节点。
这种方式正是二分查找的思想,而且查找某个节点的最大次数等同于二叉查找树的高度。
在插入节点的时候,也是利用类似的方法,通过一层一层比较大小,最终找到新节点适合插入的位置的。
但是,二叉树仍然还存在一个缺陷:就是在插入新节点的时候,可能出现下面的这种情况:
这种形态的二叉查找树,虽然也符合二叉查找树的特性,但是查找的性能大打折扣,几乎变成了线性的了。
那么这种缺陷有办法解决吗? 当然,那就是红黑树。
1 红黑树定义
红黑树 (Red Black Tree),是一种自平衡的二叉查找树。除了符合二叉查找树的基本特性之外,它还具备下列的附加特性:
- 节点是红色或黑色。
- 根节点永远是黑色。
- 每个叶子节点都是黑色的空节点(NULL节点)。
- 如果节点是红色的,则它的子节点必须是黑色的。
- 从每个叶子到根的所有路径上不能有两个连续的红色节点,当然黑色的没说不可以。
- 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
- 红黑树从根到叶子的最长路径不会超过最短路径的两倍。
我们来看一个典型的红黑树:
大家可能觉得规则还挺多的,当然也正是这些规则的束缚,才保证了红黑树的自平衡。
2 调整红黑树
当执行插入或删除节点的时候,红黑树的这些规则可能会被打破。那么这个时候我们就需要作出一些调整,以保证规则的持续有效。
首先我们需要了解一下,什么情况下会破环红黑树的规则,什么情况下不会破坏红黑树规则呢?
2.1 不会破坏红黑树规则的情况
我们向原红黑树插入值为14的新节点:
由于父节点15是黑色节点,因此这种情况并不会破坏红黑树的规则,无需做任何调整。
2.2 会破坏红黑树规则的情况
向原红黑树插入值为21的新节点:
由于父节点22是红色节点,因此这种情况打破了红黑树的 规则4(每个红色节点的两个子节点都是黑色的),必须进行调整,使之重新符合红黑树的规则。
2.3 红黑树的调整方法
- 变色
- 左旋转 (逆时针)
- 右旋转(顺时针)
注意,往往在实际的调整过程中,会综合用到多种方式,并且是重复执行。本小结最后,会介绍这几种方式在什么情况下如何选择的技巧。
2.4 变色
变色方式:为使得插入操作之后不满足规则的红黑树再次符合规则,我们可以尝试把红色节点变为黑色,黑色节点变为红色。
下图所表示的是红黑树的一部分,需要注意节点 25 并非根节点。因为节点 21 和节点 22 连续出现了红色,不符合规则4,所以把节点 22 从红色变成黑色:
然后我们发现 22 变为黑色之后,又导致了 规则 6,所以我们还需要继续进行调整,将 25 从黑色变成红色:
此时还是不行,因为节点 25 和节点 27 又形成了两个连续的红色节点,需要继续把节点 27 从红色变成黑色:
以上对于 25 为跟节点的这个子树,就调整完成了。
2.5 左旋转
左旋转 即逆时针旋转红黑树的两个节点,使得父节点被自己的右孩子节点取代,而父节点自己却成为了原父节点的左孩子节点,来看下面这张图:
图中,身为右孩子的 Y 节点取代了 身为父节点 X 节点的位置,而 X 变成了自己的左孩子。此为左旋转。
2.5 右旋转
右旋转 即顺时针旋转红黑树的两个节点,使得父节点被自己的左孩子节点取代,而父节点自己成为原父节点的右孩子。大家看下图:
图中,身为左孩子的 Y 取代了 X 的位置,而 X 变成了自己的右孩子,此为右旋转。
2.6 调整红黑树的经典例子
我们将要在下面的红黑树中插入一个节点 21 :
首先,我们需要做的是变色,即 把节点 25 及其下方的节点变色:
此时 17 和 25 是连续的两个红色节点,那么把 17 变成黑色节点可以吗?当然是不可以,这样就打破了规则4,而且根据规则2(根节点总是黑色),也不可能把节点 13 变成红色节点。
因此,变色的方式,已经无法继续解决该问题了,需要借助 左旋转 对 13 和 17 两个节点进行一下调整:
那么由左旋转的机制可知,我们需要将 17 作为红黑树的根节点 ,13 不再是根结点,而是需要调整为 17 根结点的左孩子节点。而 15 这个子树则需要调整为 13 节点的右孩子节点。调整之后的红黑树就像下面这样:
由于根节点 17 必须是黑色节点,所以需要变色,同时 13 节点下的左子树也都要进行变换 (注意:叶节点不动,始终为黑色),变色后的结果如下:
那么现在的红黑树,是不是就OK了呢?当然没有,因为 (17 -> 8 -> 6 -> Null) 的黑色节点数是 4,而其他路径下的黑色节点个数是 3,明显违背了 规则 6。所以我们还要继续调整。
接下来我们需要采用 右旋转 的方式, 对 13 和 8 两个节点进行转换:
转换细节为:我们需要将 8 作为 根节点 17 的左孩子节点,13 作为 8 的右孩子节点,并且需要将 11 作为 13 的左孩子节点。变换之后的红黑树就变成了下面这样:
最后我们还需要再做一次 颜色转换的操作,所以最终的红黑树变成了下面的这样:
如此一来,我们的红黑树变得重新符合规则。这一个例子的调整过程比较复杂,经历了如下步骤:变色 -> 左旋转 -> 变色 -> 右旋转 -> 变色 。
3 红黑树中换色、左旋转、有旋转技巧
其实红黑树的关键玩法就是弄清楚这三种调整方式的规则。
假设我们插入的新节点为 X:
- 将新插入的节点标记为红色
- 如果 X 是根结点(root),则标记为黑色
- 如果 X 的 parent 不是黑色,同时 X 也不是 root:
- 3.1 如果 X 的 uncle (叔叔) 是红色
- 3.1.1 将 parent 和 uncle 标记为黑色
- 3.1.2 将 grand parent (祖父) 标记为红色
- 3.1.3 让 X 节点的颜色与 X 祖父的颜色相同
就像下面这张图:
跟着上面的公式走:
- 将新插入的 X 节点标记为红色
- 发现 X 的 parent (P) 同样为红色,这违反了红黑树的第三条规则「不能有两个连续相邻的红色节点」
- 发现 X 的 uncle (U) 同样为红色
- 将 P 和 U 标记为黑色
- 将 X 和 X 的 grand parent (G) 标记为相同的颜色,即红色,继续重复公式 2、3
- 发现 G 是根结点,标记为黑色
- 结束
刚刚说了 X 的 uncle 是红色的情况,接下来要说是黑色的情况
- 3.2 如果 X 的 uncle (叔叔) 是黑色,我们要分四种情况处理
- 3.2.1 左左 (P 是 G 的左孩子,并且 X 是 P 的左孩子)
- 3.2.2 左右 (P 是 G 的左孩子,并且 X 是 P 的右孩子)
- 3.2.3 右右 (和 3.2.1 镜像过来,恰好相反)
- 3.2.4 右左 (和 3.2.2 镜像过来,恰好相反)
当出现 uncle 是黑色的时候我们第一步要考虑的是 旋转 ,这里先请小伙伴不要关注红黑树的第 6 条规则,主要是为了演示如何旋转的:
左左情况
这种情况很简单,想象这是一根绳子,手提起 P 节点,然后变色即可
左右情况
左旋: 使 X 的父节点 P 被 X 取代,同时父节点 P 成为 X 的左孩子,然后再应用 左左情况
右右情况
与左左情况一样,想象成一根绳子
右左情况
右旋: 使 X 的父节点 P 被 X 取代,同时父节点 P 成为 X 的右孩子,然后再应用 右右情况
4 红黑树的应用
TreeMap、TreeSet以及JDK1.8的HashMap底层都用到了红黑树。