前言
提到红黑树,我们首先得知道二叉查找树(Binary Search Tree),也称二叉搜索树,有序二叉树,排序二叉树,是指一颗空树,或者具有以下性质的二叉树:
(1)若任意节点的左子树不空,则左子树上所有节点的值均小于它的根节点的值;
(2)若任意节点的右子树不空,则右子树上的所有节点的值均大于它的根节点的值;
(3)任意节点的左、右子树也分别为二叉查找树;
(4)没有键值相等的节点。
二叉查找树的思想正是二分查找的思想,查找所需的最大次数等于二叉查找树的高度;正常情况下的查找效率还不错,但是却也有其致命的缺陷:当最坏的情况下,当先后插入的关键字有序时,构成的二叉查找树蜕变成单支树,树的深度为n,其平均查找长度为(n+1)/ 2(和顺序查找相同),失去了其本身在查找上的优势。如下图:
所以,产生了一种新的数据结构-红黑树;在下文中对红黑树作了简单的介绍。
1、概念
红黑树是一种自平衡的二叉查找树。
2、性质
红黑树是每个节点都带有颜色属性的二叉查找树,颜色为红色或黑色。在二叉查找树强制一般要求以外,对于任何有效的红黑树我们增加了如下的额外要求:
1.节点是红色或黑色。
2.根是黑色。
3.所有叶子都是黑色(叶子是NIL节点)。
4.每个红色节点必须有两个黑色的子节点。(从每个叶子到根的所有路径上不能有两个连续的红色节点。)
5.从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点。
下面是一个具体的红黑树的图例:
这些约束确保了红黑树的关键特性:从根到叶子的最长的可能路径不多于最短的可能路径的两倍长。结果是这个树大致上是平衡的。因为操作比如插入、删除和查找某个值的最坏情况时间都要求与树的高度成比例,这个在高度上的理论上限允许红黑树在最坏情况下都是高效的,而不同于普通的二叉查找树。
为什么说这些性质确保了这个结果?
答:注意到性质4导致了路径不能有两个毗连的红色节点就足够了。最短的可能路径都是黑色节点,最长的可能路径有交替的红色和黑色节点。因为根据性质5所有最长的路径都有相同数目的黑色节点,这就表明了没有路径能多于任何其他路径的两倍长。
3、平衡处理
当我们在对红黑树进行插入和删除等操作时,对树做了修改,则可能违背红黑树的性质。例如:向原红黑树中插入一个关键字。
向原红黑树中插入21时,由于父节点22也是红色节点,因此,这种情况打破了红黑树的性质4(从每个叶子到根的所有路径上不能有两个连续的红色节点。)
所以,我们需要做出一些调整,才能保证一颗红黑树始终是一颗红黑树。调整有两种方法:变色和旋转。
变色
为了重新符合红黑树的规则,尝试把红色节点变为黑色,或者把黑色节点变为红色。
下图所表示的是红黑树的一部分,因为节点21和节点22连续出现了红色,不符合规则4,所以把节点22从红色变成黑色:
但这样并不算完,因为凭空多出的黑色节点打破了规则5,所以发生连锁反应,需要继续把节点25从黑色变成红色:
此时仍未完,因为节点25和27又形成了两个连续的红色节点,需要继续把节点27从红色变成黑色:
至此,这个红黑树才算平衡完毕,重新符合红黑树的要求。
旋转
树的旋转,分为左旋和右旋。
a、左旋:
逆时针旋转红黑树的两个节点,使父节点被自己的右孩子所取代,而自己成为自己的左孩子;
左旋的伪代码《算法导论》:参考伪码,可以更好的理解左旋的思路。
LEFT-ROTATE(T, x)
01 y ← right[x] // 前提:这里假设x的右孩子为y。下面开始正式操作
02 right[x] ← left[y] //将“y的左孩子”设为“x的右孩子”,即将β设为x的右孩子
03 p[left[y]] ← x //将 “x” 设为 “y的左孩子的父亲”,即 将β的父亲设为x
04 p[y] ← p[x] // 将 “x的父亲” 设为 “y的父亲”
05 if p[x] = nil[T]
// 情况1:如果 “x的父亲” 是空节点,则将y设为根节点
06 then root[T] ← y
07 else if x = left[p[x]]
// 情况2:如果 x是它父节点的左孩子,则将y设为“x的父节点的左孩子”
08 then left[p[x]] ← y
// 情况3:(x是它父节点的右孩子) 将y设为“x的父节点的右孩子”
09 else right[p[x]] ← y
10 left[y] ← x // 将 “x” 设为 “y的左孩子”
11 p[x] ← y // 将 “x的父节点” 设为 “y”
b、右旋:
顺时针旋转红黑树的两个节点,使得父节点被自己的左孩子所取代,而自己成为自己的右孩子;
右旋的伪代码《算法导论》:参考伪码,可以更好的理解右旋的思路。
RIGHT-ROTATE(T, y)
01 x ← left[y] // 前提:这里假设y的左孩子为x。下面开始正式操作
02 left[y] ← right[x] //将“x的右孩子”设为 “y的左孩子”,即将β设为y的左孩子
03 p[right[x]] ← y //将 “y” 设为 “x的右孩子的父亲”,即 将β的父亲设为y
04 p[x] ← p[y] // 将 “y的父亲” 设为 “x的父亲”
05 if p[y] = nil[T]
//情况1:如果 “y的父亲” 是空节点,则将x设为根节点
06 then root[T] ← x
07 else if y = right[p[y]]
// 情况2:如果 y是它父节点的右孩子,则将x设为“y的父节点的左孩子”
08 then right[p[y]] ← x
// 情况3:(y是它父节点的左孩子) 将x设为“y的父节点的左孩子”
09 else left[p[y]] ← x
10 right[x] ← y // 将 “y” 设为 “x的右孩子”
11 p[y] ← x // 将 “y的父节点” 设为 “x”
在具体的应用中,当仅靠变色无法达到平衡的情况下,就需要使用旋转来使平衡红黑树,而往往我们是将两种情况混合使用,灵活对待。
4、应用与优势
JDK的集合类TreeMap和TreeSet底层就是红黑树实现的。在Java8中,连HashMap也用到了红黑树。
红黑树和AVL树一样都对插入时间、删除时间和查找时间提供了最好可能的最坏情况担保。这不只是使它们在时间敏感的应用如实时应用(real time application)中有价值,而且使它们有在提供最坏情况担保的其他数据结构中作为建造板块的价值;例如,在计算几何中使用的很多数据结构都可以基于红黑树。
红黑树在函数式编程中也特别有用,在这里它们是最常用的持久数据结构(persistent data structure)之一,它们用来构造关联数组和集合,每次插入、删除之后它们能保持为以前的版本。除了 O(log n)的时间之外,红黑树的持久版本对每次插入或删除需要 O(log n)的空间。