红黑树 |
定义
红黑树,顾名思义,就是树的节点只有红色和黑色两种状态,通过这两种状态的标识和规定颜色的使用,来使树达到相对平衡。为什么说相对平衡?因为在红黑树中,所有的条件限制只能保证,所有路径中最长的路径不能使最短路径的2倍。只能保证相对平衡。
有人讨论:红黑树是不是一棵平衡二叉树?因为红黑树不满足严格的平衡二叉树的定义,从严格意义上来讲,红黑树并不是平衡二叉树;但是,红黑树在建立的时候,也是有平衡调整的过程,是相对平衡的,所以它也不是严格的不平衡的,所以老祖宗的语言的艺术就有用了,应该说,红黑树是一棵自平衡二叉树(不要说它是平衡二叉树),只能保证相对平衡。
性质
(1)每个节点或者是黑色,或者是红色。
(2)根节点是黑色。
(3)每个叶子节点(NIL)是黑色。 [注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!]
(4)如果一个节点是红色的,则它的子节点必须是黑色的。
(5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。
注意:
(01) 特性(3)中的叶子节点,是只为空(NIL或null)的节点。
(02) 特性(5),确保没有一条路径会比其他路径长出俩倍。因而,红黑树是相对是接近平衡的二叉树。
应用
红黑树广泛的应用在各种程序库中,主要是用它来存储有序的数据,它的时间复杂度是O(lgn),效率非常之高。在linux内核的进行调度上面,C++中stl中的很多容器多采用了红黑树的算法,Java中的TreeMap也是采用了红黑树的算法来实现排序。
基本操作
旋转
红黑树的旋转相比二叉平衡树,要简单一点,因为其只有两种旋转操作,即左旋和右旋。
左旋
是不是感觉上面的左旋很熟悉,其实就是平衡二叉树中的右右的这种情况,用 x 右节点代替 x,其右节点的右子树保持不变,并且将其右节点的左子树赋给 x 的右子树,完毕。
右旋
与左旋完全相反,用 y 左节点代替 y,其左节点的左子树保持不变,并且将其左节点的右子树赋给 y 的左子树,完毕。
如何区分右旋 左旋
这是一个问题,但是仔细观察上面,就是左旋就是让旋转的节点变成左子树,右旋就是让旋转的节点变成右子树,简单的总结一句话,
左旋–>提右节点,变左子树 |
---|
右旋–>提左节点,变右子树 |
添加
将一个节点插入到红黑树中,首先,将红黑树当作一颗二叉查找树,将节点插入;然后,将节点着色为红色;最后,通过旋转和重新着色等方法来调整该树,使之重新成为一颗红黑树。详细描述如下:
第一步: 将红黑树当作一颗二叉查找树,将节点插入。
红黑树本身就是一颗二叉查找树,将节点插入后,该树仍然是一颗二叉查找树。也就意味着,树的键值仍然是有序的。此外,无论是左旋还是右旋,若旋转之前这棵树是二叉查找树,旋转之后它一定还是二叉查找树。这也就意味着,任何的旋转和重新着色操作,都不会改变它仍然是一颗二叉查找树的事实。
第二步:将插入的节点着色为”红色”。
为什么着色成红色,而不是黑色呢?将插入的节点着色为红色,不会违背”特性(5)”!少违背一条特性,就意味着我们需要处理的情况越少。接下来,就要努力的让这棵树满足其它性质即可;满足了的话,它就又是一颗红黑树了。
第三步: 通过一系列的旋转或着色等操作,使之重新成为一颗红黑树。
第二步中,将插入节点着色为”红色”之后,不会违背”特性(5)”。那它到底会违背哪些特性呢?
对于”特性(1)”,显然不会违背了。因为我们已经将它涂成红色了。
对于”特性(2)”,显然也不会违背。在第一步中,我们是将红黑树当作二叉查找树,然后执行的插入操作。而根据二叉查找数的特点,插入操作不会改变根节点。所以,根节点仍然是黑色。
对于”特性(3)”,显然不会违背了。这里的叶子节点是指的空叶子节点,插入非空节点并不会对它们造成影响。
* 对于”特性(4)”*,是有可能违背的!
那接下来,想办法使之”满足特性(4)”,就可以将树重新构造成红黑树了。
参考下面的伪码:
RB-INSERT(T, z)
y ← nil[T] // 新建节点“y”,将y设为空节点。
x ← root[T] // 设“红黑树T”的根节点为“x”
while x ≠ nil[T] // 找出要插入的节点“z”在二叉树T中的位置“y”
do y ← x
if key[z] < key[x]
then x ← left[x]
else x ← right[x]
p[z] ← y // 设置 “z的父亲” 为 “y”
if y = nil[T]
then root[T] ← z // 情况1:若y是空节点,则将z设为根
else if key[z] < key[y]
then left[y] ← z // 情况2:若“z所包含的值” < “y所包含的值”,则将z设为“y的左孩子”
else right[y] ← z // 情况3:(“z所包含的值” >= “y所包含的值”)将z设为“y的右孩子”
left[z] ← nil[T] // z的左孩子设为空
right[z] ← nil[T] // z的右孩子设为空。至此,已经完成将“节点z插入到二叉树”中了。
color[z] ← RED // 将z着色为“红色”
RB-INSERT-FIXUP(T, z) // 通过RB-INSERT-FIXUP对红黑树的节点进行颜色修改以及旋转,让树T仍然是一颗红黑树
总体上就是一个二叉搜索树的插入,主要的调整请看我们下面的关于RB-INSERT-FIXUP的伪码:
RB-INSERT-FIXUP(T, z)
while color[p[z]] = RED // 若“当前节点(z)的父节点是红色”,则进行以下处理。
do if p[z] = left[p[p[z]]]