红黑树是一种二叉查找树,在每个节点上增加一个存储位表示节点的颜色,可以是RED或BLACK。通过对任何一条从根到叶子的路径上各个节点的着色方式的限制,红黑树确保没有一条路径会比其它路径长出两倍,因为是接近平衡的,在实际中红黑树具有较好的平衡性。
为了方便红黑树的操作,树中每个节点包含5个域:color、key、left、right和p。如果某个节点没有子节点或父节点,则该节点的left、right和p指针域应该设置为NULL,将NULL视为指向红黑树的外节点(叶子)的指针,而把带关键字的节点视为树的内节点。
一个二叉查找树如果满足如下红黑性质,则为一棵红黑树:
1.每个节点是RED或BLACK。
2.根节点(root)是BLACK。
3.每个叶子节点(NULL)是BLACK。
4.如果一个节点是RED,则它的两个儿子都是BLACK。
5.对每个节点,从该节点到任意一个叶子节点的路径上包含的BLACK节点的数目相同。
为了方便处理红黑树代码中的边界条件,可以采用一个哨兵nil[T]来代表NULL,对一棵红黑树T来说,哨兵是一个与树内普通节点(前文中的内节点)有相同域的对象,它的color域为BLACK,而它的其它域,如p,key,left,right,可以取任意合理的值,如p指向其父节点,left,right,key可以不设置具体值。在红黑树中所有指向NULL的指针将被替换为指向nil[T],并采用一个哨兵来代替所有的NULL(包括根节点的父节点以及所有的叶子节点),这样做不仅可以节省空间,而且可以方便边界条件的处理。
如下图所示,a)、b)和c)分别表示采用三种不同方式表示的红黑树,a)中叶子节点用NULL表示,b)中采用哨兵表示,c)叶子和根节点的父节点全部被忽略:
根据红黑树的性质,我们可以看到如下特点:
1.一棵有n个内节点的红黑树的高度至多为2lg(n+1)(具体证明请参考算法导论中文版第164页),但是通常RB-tree(红黑树)具有良好的平衡性能,
RB-tree的搜索平均效率与AVL-tree(平衡二叉树)几乎相等。中序遍历为递增有序序列。
2.一般情况下,假定新插入的节点的颜色为RED,这样做可以使条件5得以满足,但是可能会破坏条件2或4,因此需要进行适当的调整,
以满足红黑树的条件。
3.由于红黑树是一棵二叉查找树,因此在删除某个节点的时候,当这个节点是叶子节点时(按照上面的表示方式,该节点的左右子节点均为哨兵),
可以直接删除,如果不是叶子节点,则视具体情况删除其子节点或中序遍历的后继节点。在删除一个节点后,仍需根据实际情况进行调整。
4.根据RB-tree性质,下面的情况不会出现:
这些情况会破坏RB-tree的性质5,因此只会出现单只BLACK-RED或有两个子节点。
无论是在RB-tree中插入节点还是删除节点,都有可能进行RB-tree的调整操作,则旋转操作是调整操作的重要步骤,当在某个节点x上做左旋操作时,
假设其右孩子y不是nil[T],x可以是树内任意右孩子不是nil[T]的节点,左旋以x到y之间的链为轴进行旋转,使y成为该子树新的根,x成为y的左孩子,
y的左孩子成为x的右孩子。左旋和右旋的操作可以用下图进行表示:
左旋和右旋操作的伪代码如下所示:
//左旋操作
LEFT-ROTATE(T,x):
y=right[x]
right[x]=left[y]
if left[y]≠nil[T]
then 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]
then 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
红黑树的查找操作类似于二叉查找树的查找操作,沿着根节点到某个叶子节点的路径进行查找,
直到找到包含特定关键字的节点或遍历完整条路径没有找到而失败结束。
红黑树的构造过程是不断在插入节点的过程,在插入节点的过程中,会破坏红黑树的性质,导致需要对树的局部结构进行调整,
以满足红黑树的性质。当插入一个节点时,节点插入的位置是叶子,且被置为RED,并且需要调用调整操作对节点进行旋转及重新着色。
当插入一个节点是可能破坏的红黑树性质是2和4,首先若插入的节点是根节点,那么此时根节点的颜色为RED,不满足性质2,
在这种情况下,只要将该根节点的颜色改为BLACK即可;如新插入的节点的父节点的颜色也为RED时,此时违反了性质4,视具体情况进行调整和重新着色操作。
下面是节点插入和调整操作伪代码:
//红黑树的插入操作
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]<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,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
上图为在红黑树中插入节点z时违反性质4时进行的调整操作(其中错误部分已经进行改正,字母表示节点的正确颜色)。