红黑树(red-black tree) 是许多“平衡的”查找树中的一种,它能保证在最坏情况下,基本的动态集合操作的时间为O(lgn) 。
红黑树的性质:
红黑树是一种二叉查找树,但在每个结点上增加一个存储位表示结点的颜色,可以是RED或BLACK 。通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出两倍,因而是接近平衡的。
树中每个结点包含五个域: color, key, left, right和p。如果某结点没有一个子结点或父结点,则该结点相应的指针(p)域包含值NIL。我们将把这些NIL视为指向二叉查找树的外结点(叶子)的指针,而把带关键字的结点视为树的内结点。
红黑树有以下五点性质:
1、每个节点或是红色的,或是黑色的;
2、根节点是黑色的;
3、每个叶节点(NIL)是黑色的;
4、如果一个节点是红色的,则其两个子节点都是黑色的;
5、对于每个节点,从该节点到其所有后代叶节点的简单路径上,均包含相同数目的黑色节点。
当满足以上5个性质的红黑树,就能保证没有一条路径会比其他路径长2倍以上,因而可看作近似平衡。
黑高度:
从某个节点x出发到大一个叶节点的任意一条路径上,黑色节点的个数称为该节点的黑高度,用bh(x)表示。
以任一结点x为根的子树至少包含2的bh(x)次方−1个内部结点。
红黑树是一种好的二叉查找树,对一棵有n个内节点的红黑树的高度至多为2lg(n+1),STL中的set和map就是用红黑树实现的。红黑树的动态集合操作SEARCH, MINIMUM, MAXIMUM, SUCCESSOR, PREDECESSOR与二叉查找树的对应操作的实现一样,且它们的时间复杂度都是O(lgn)。
为了便于处理红黑树代码中的边界条件,我们采用一个哨兵来代表NIL。对一棵红黑树T 来说,哨兵nil[T] 是一个与树内普通结点有相同域的对象。它的color 域为BLACK, 而它的其他域p, left, right 和key可以设置成任意允许的值。如下图所示,所有指向NIL的指针都被替换成指向哨兵nil[T]的指针。
使用哨兵后,就可以将结点x的NIL孩子视为一个其父结点为x的普通结点。虽然我们可以在树内的每一个NIL 上新增一个不同的哨兵结点,来让每个NIL的父结点都有这样的定义,但是这种做法会浪费空间。
我们的做法是采用一个哨兵nil[T]来代表所有的NIL——所有的叶子以及根部的父结点。哨兵的域p, left, right 以及key 的取值如何并不重要,为了方便起见,也可以在程序中设定它们。
在本章中,我们使用忽略了所有叶子与根部的父节点的红黑树。
如:
旋转:
两种旋转: 左旋和右旋。
如:
当在某个结点x上做左旋时,我们假设它的右孩子y不是nil[T]; x可以为树内任意右孩子不是nil[T]的结点。
左旋过后,使y的父结点从x变为x的父结点,x变为y的右子结点,y的左子结点变为x的右子结点。
伪代码:
LEFT-ROTATE(T,x)
y <- right[x]
right[x] <- left[y]
if left[y]!= nil[T]
p[left[y]] <- x
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
left[y] <- x
p[x] <- y
RIGHT-ROTATE(T,x)
y <- left[x]
left[x] <- right[y]
if right[y] != nil[T]
p[right[y]] <- x
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
right[y] <- x
p[x] <- y
下面举一个例子:
插入:
向一棵含n 个结点的红黑树中插入一个新结点的操作可在O(lgn) 时间内完成。
我们利用TREE-INSERT 过程的一个略作修改的版本,来将结点z插入树T内,就好像T是一棵普通的二叉查找树一样,然后将z着为红色。为保证红黑性质能继续保持,我们调用一个辅助程序RB-INSERT-FIXUP 来对结点重新着色并旋转。
红黑树有以下五点性质:
1、每个节点或是红色的,或是黑色的;
2、根节点是黑色的;
3、每个叶节点(NIL)是黑色的;
4、如果一个节点是红色的,则其两个子节点都是黑色的;
5、对于每个节点,从该节点到其所有后代叶节点的简单路径上,均包含相同数目的黑色节点。
z插入后有以下几种情况:
z作为根节点:(破坏了性质2)修改根节点为黑色即可;
z的父节点为黑色: 不做操作;
z的父节点为红色:(破坏了性质4)又分成三种情况:
1、z的叔结点y是红色;
2、z的叔结点y是黑色且z是一个右孩子;
3、z的叔结点y是黑色且z是一个左孩子。
情况1、z的叔结点y是红色:
如:
如果z的叔节点是红色,则z结点的祖父结点是黑色,父结点是红色(父结点和本身都是红色才会违背性质4)。我们只需要将父、叔结点涂成黑色,祖父结点涂成红色,而本身颜色不需要改变。
这样一来只是维护好了自己的“小家庭”,包括父、叔结点和祖父节点。而且保证了性质⑤:红变黑、黑变红,并不会增减某一条路径中黑结点的个数。至于祖父节点的变色可能会导致它和它父亲结点的冲突,所以我们就将“当前结点”这个“不合群”的帽子扣在了祖父结点,让它继续地被维护。
情况2、z的叔结点y是黑色且z是一个右孩子:
将z的父结点做为新的当前结点,将新的当前结点做左旋。这样就将情况2转到情况3来处理。
情况3:z的叔结点y是黑色且z是一个左孩子:
如:
如图,情况2中将z的父结点做为新的当前结点,将新的当前结点做左旋。这样就变成了情况3。再将此时的z父结点做为新的当前结点,将新的当前结点涂成黑色(之前是红色),新的当前节点的父节点涂成红色,再对新的当前结点的父结点做右旋。这样就保证了性质5,同时修正了性质4,中间并没有破坏任何其他性质,结束循环判断。
伪代码:
RB-INSERT(T,x)
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] < key[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(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[p[z]] <- BLACK
color[y] <- BLACK
color[p[p[z]]] <- RED
z <- p[p[z]]
else if z == right[[p[z]]
then z <- p[z]
LEFT-ROTATE(T,x)
color[p[z]] <- BLACK
color[p[p[z]]] <- RED
RIGHT-ROTATE(T,z.p.p)
else(same as then clause with "right" and "left" exchanged)
color[root[T]] <- BLACK
删除:
和n个结点的红黑树上的其他基本操作一样,对一个结点的删除要花O(lgn) 时间。
程序RB-DELETE是对TREE-DELETE程序略作修改得来的。在删除一个结点后,该程序就调用一个辅助程序RB-DELETE-FIXUP,用来改变结点的颜色并做旋转,从而保持红黑树性质。
删除操作也分几个步骤:
首先,按照搜索二叉树的删除操作删除要删的结点,然后针对每种情况做换色和旋转操作,使其恢复红黑树的性质。
红黑树有以下五点性质:
1、每个节点或是红色的,或是黑色的;
2、根节点是黑色的;
3、每个叶节点(NIL)是黑色的;
4、如果一个节点是红色的,则其两个子节点都是黑色的;
5、对于每个节点,从该节点到其所有后代叶节点的简单路径上,均包含相同数目的黑色节点。
换色和旋转的维护工作分为4种情况考虑(以下情况是x作为其父节点的左孩子的情况,右孩子的情况对称处理):
1、待删结点x的兄弟结点w是红色:
如图,x的兄弟结点w是红色,所以它一定有两个黑色的子结点。我们只需改变待删的兄弟结点为黑色,改变父结点的颜色为红色,然后再对父结点做一次左旋操作,所以待删结点的兄弟结点一定是黑色。这样就将情况1转为情况2、3、4之一去处理。
2、待删结点x的兄弟结点w是黑色,且w的两个子结点都是黑色:
如图,x的兄弟节点w是黑色,且w的两个子节点都是黑色。这种情况下B左边黑色高度比右边的少1,所以只需将w变成红色就满足性质5。
如果父节点B本身是黑色的,这时父节点B具有黑黑的属性,继续当做x迭代处理!
如果父节点B本身是红色的,这时父节点B具有红黑的属性,直接退出循环,把父节点B涂成黑色即可!
3、待删结点x的兄弟结点w是黑色,且w的左孩子是红色,右孩子是黑色:
我们需要交换w和其左孩子的颜色,即C、D颜色互换。然后对w进行右旋,使得待删结点x的新兄弟结点new w是一个有红色右孩子的黑色结点,这样将情况3转为情况4去处理。
4、待删结点x的兄弟结点w是黑色,且w的右孩子是红色:
此时我们可以将兄弟结点D染成当前父结点B的颜色,把当前父结点B的颜色染成黑色,兄弟结点的右子结点E染成黑色。然后对当前父结点B做一次左旋即可。
伪代码:
RB-TRANSPLANT(t,u,v)
if u.p == T.nil
T.root = v
else if u == u.p.left
u.p.left = v
else u.p.right = v
v.p = u.p
RB-DELETE(T,z)
y = z
y-original-color = y.color
if z.left == T.nil
x = z.right
RB-TRANSPLANT(T,z,x)
else if z.right == T.nil
x = z.left
RB-TRANSPLANT(T,z,x)
else y = TREE-MINIMUM(z.right)
y-original-color = y.color
x = y.right
if y.p == z
x.p = y
else
RB-TRANSPLANT(T,y,x)
y.right = z.right
z.right.p = y
RB-TRANSPLANT(T,z,y)
y.left = z.left
y.left.p = y
y.color = z.color
if y-original-color == BLACK
RB-DELETE-FIXUP(T,x)
RB-DELETE-FIXUP(T,x)
while x != T.root && x.color == BLACK
if x == x.p.left
w = x.p.right
if w.color == RED
w.color = BLACK
x.p.color = RED
LEFT_ROTATE(T,x.p)
w = x.p.right
if w.left.color == BLACK && w.right.color == BLACK
w.color = RED
x = x.p
else
if w.right.color == BLACK
w.left.color = BLACK
w.color = RED
RIGHT-ROTATE(T,w)
w = x.p.right
w.color = x.p.color
x.p.color = BLACK
w.right.color = BLACK
LEFT_ROTATE(T,x.p)
x = T.root
else(same as then clause with "right" and "left" exchanged)
T.root.color = BLACK