第十三章 红黑树
总结:这章介绍了红黑树的性质、旋转,并详细介绍了红黑树的插入和删除。
1. 红黑树的性质
1) 每个节点或是红的,或是黑的
2) 根节点是黑的
3) 每个叶节点(NIL)是黑的
4) 如果一个节点是红的,则它的两个儿子都是黑的
5) 对每个节点,从该节点到其子孙节点的所有路径上包含相同数目的黑节点。
黑高度:从某个节点x出发到大一个叶节点的任意一条路径上,黑色节点的个数称为该节点的黑高度,用bh(x)表示。
红黑树是一种好的二叉查找树,对一棵有n个内节点的红黑树的高度至多为2lg(n+1),STL中的set和map就是用红黑树实现的。
红黑树的动态集合操作SEARCH, MINIMUM, MAXIMUM, SUCCESSOR, PREDECESSOR与二叉查找树的对应操作的实现一样,且它们的时间复杂度都是O(lgn)
2. 旋转
复杂度O(1)
左旋:
伪代码
LEFT-ROTATE(T,x)
y <- right[x]
if left[y]!=NIL[T]
then p[left[y]] <- x
right[x] <- left[y]
p[y] <- p[x]
if p[x]=NIL[T]
then root[T] <- y
else if x=left[p[x]]
then left[p[x]] <- y
else right[p[x]] <- y
p[x] <- y
left[y] <- x
右旋:
伪代码
RIGHT-ROTATE(T,y)
x <- left[y]
left[y] <- right[x]
if right[x]!=NIL[T]
p[right[x]] <- y
p[x] <- p[y]
if p[y]=NIL[T]
then root[T] <- x
else if y=left[p[y]]
then left[p[y]] <- x
else right[p[y]] <- x
p[y] <- x
right[x] <- y
3. 插入
时间复杂度O(lgn)
插入操作类似二叉查找树的插入,然后将插入的节点的颜色设为RED,由于插入操作可能破坏红黑树的性质2(当插入的是根节点时)和性质4(当父节点为红色节点时),因此需要额外的RB-INSERT-FIXUP操作来进行红黑树性质的保持。
伪代码
RB-INSERT(T,z)
y <- NIL[T]
x <- root[T]
while x!=NIL[T]
do y <- x
if key[z] < key[x]
then x <- left[x]
else x <- right[x]
p[z] <- y
if y=NIL[T]
then root[T] <- z
else if key[z] < left[y]
then left[y] <- z
else right[y] <- z
left[z] <- NIL[T]
right[z] <- NIL[T]
color[z] <- RED
RB-INSERT-FIXUP(T,z)
分情况讨论RB-INSERT-FIXUP:
首先,如果红黑树的性质2遭到破坏,那么只要简单的将根节点改为黑色即可。
其次,如果红黑树的性质4遭到破坏,即插入节点的父节点也为红节点,那么该节点一定有祖父节点,否则就不满足性质2,且该节点的祖父节点一定为黑色,否则不满足性质4。
设插入节点是z
这里有两组可能性,一是p[z]是p[p[z]]的左儿子,二是p[z]是p[p[z]]的右儿子。每组情况中又有三种子情况。仅讨论第一组情况。
在第一组条件下,又有三种情况:
Case1: z的叔父节点(即p[p[z]]的右儿子)的颜色为红色,则将z的父节点及z的叔父节点的颜色改为黑色,将z的祖父节点的颜色改为红色,然后将z上升至祖父节点,继续循环,保持红黑树的性质。
Case2: z的叔父节点的颜色为黑色,且z是p[z]的右儿子。对于这种情况,我们将Case2变为Case3,然后再处理Case3即可,即将z变为p[z]的左儿子。这可以通过将以p[z]为根的子树进行左旋,然后z指向原来的p[z],这样情况变为Case3。
Case3: 将p[p[z]]设为红色,将p[z]设为黑色,然后对以p[p[z]]为根的子树进行右旋。
伪代码
RB-INSERT-FIXUP(T,z)
while color[p[z]]=RED
do if p[z]=left[p[p[z]]]
then y <- right[p[p[z]]]
if color[y]=RED
then color[y] <- BLACK
color[p[z]] <- BLACK
color[p[p[z]]] <- RED
z <- p[p[z]]
else
then if z=right[p[z]]
then z <- p[z]
LEFT-ROTATE(T,z)
color[p[z]] <- BLACK
color[p[p[z]]] <- RED
RIGHT-ROTATE(T,p[p[z]])
else (same as then clause with “right” and “left” exchanged)
color[root[T]] <- BLACK
4. 删除
时间复杂度O(lgn)
红黑树的删除操作类似二叉查找树的删除操作,但是当删除的节点是黑色节点时,性质2(删除的是根节点,且一个红色孩子成为了新的根节点)、性质4(删除节点的父节点和孩子节点都为红色)、性质5(显然删除一个黑节点后,性质5无法满足)可能遭到破坏。因此,需要额外的RB-DELETE-FIXUP操作来进行红黑树性质的保持。
伪代码
RB-DELETE(T,z)
if left[z]=NIL[T] or right[z]=NIL[T]
then y <- z
else y <- TREE-SUCCESSOR(z)
if left[y]!=NIL[T]
then x <- left[y]
else x <- right[y]
p[x] <- p[y]
if p[x]=NIL[T]
then root[T] <- x
else if y=left[p[y]]
then left[p[y]] <- x
else right[p[y]] <- x
if y!=z
then key[z] <- key[y]
if color[y]=BLACK
then RB-DELETE-FIXUP(T,x)
return y
这里,x为y删除后替换上去的一个节点。如果x为红色的话,那么只要将x变为黑色即可。
但是如果x为黑色的话,情况就复杂了。仍然有两组情况,一是x为p[x]的左儿子,一是x为p[x]的右儿子。仅讨论x是p[x]左儿子的情况。
设w为x的兄弟节点,即w为p[x]的右儿子,且w一定不是nil[T],否则原来的红黑树中不满足性质5。
分四种情况讨论:
Case1: w是红色的。这种情况下,p[x]一定为黑色的。w的左右儿子一定是黑色的,将Case1转换为Case2的情况,对以p[x]为根的树进行左旋,将p[x]设为红色,原来的w设为黑色,则红黑性质保持,且问题转换为Case2,3,或4。
Case2: w是黑色的,如果w的两个儿子都是黑色的,那么将w改为红色,那么从p[x]出发经过左儿子到叶子节点的路径上与经过右儿子到叶子节点的路径上的黑色节点个数相同了(因为删去y后,左边的路径上的黑节点少掉了一个,而把w的颜色改掉后,右边的路径上的黑节点也少了一个,因此数量仍旧相等),但是对p[p[x]]而言,经过p[x]的路径上的黑色节点的个数少了一个,又必须重新保持红黑树性质。因此,此时x新指向p[x],并重新循环。这种情况简单的说,就是左边少了个黑色,所以右边也减去个黑色的,然后整体就少了个黑色的,因此循环下去。
Case3: w是黑色的,w的左儿子是红色的,右儿子是黑色的。对以w为根的树进行右旋,将left[w]设为黑色的,w设为红色,新的w指向原来的left[w],问题转变为Case4。
Case4:w是黑色的,w的右儿子是红色的。通过改变一些节点的颜色,并执行一次左旋,可以将x的额外的黑色去除。这种情况就是左边少了个黑色的,然后往左边又加个黑色的。
伪代码
RB-DELETE-FIXUP(T,z)
while x!=root[T] and color[x]=BLACK
do if x=left[p[x]]
then w <- right[p[x]]
if color[w]=RED
then color[w] <- BLACK
color[p[x]] <- RED
LEFT-ROTATE(T,p[x])
w <- right[p[x]]
if color[left[w]]=BLACK and color[right[w]]=BLACK
then color[w] <- RED
x <- p[x]
else
then if color[right[w]]=BLACK
then color[left[w]] <- BLACK
color[w] <- RED
RIGHT-ROTATE(T,w)
w <- right[p[x]]
color[w] <- color[p[x]]
color[p[x]] <- BLACK
color[right[w]] <- BLACK
LEFT-ROTATE(T,p[x])
x <- root[T]
else (same as then clause with “right” and “left” exchanged)
color[x] <- BLACK
附录:
C++代码