二 红黑树(Red-Black Tree)
在上一篇博客中已经比较完整地介绍了BST(Binary Search Tree)的基本性质和各种操作的代码实现,对BST有较深刻的理解后再理解RBT(Red-Black Tree)就不会很吃力了。
首先简单了解一下什么是RBT,来自百度百科:红黑树是一种自平衡二叉查找树,是在计算机科学中用到的一种数据结构,典型的用途是实现关联数组。它是在1972年由Rudolf Bayer发明的,他称之为"对称二叉B树",它现代的名字是在 Leo J. Guibas 和 Robert Sedgewick 于1978年写的一篇论文中获得的。它是复杂的,但它的操作有着良好的最坏情况运行时间,并且在实践中是高效的:它可以在O(log n)时间内做查找,插入和删除,这里的n是树中元素的数目。
从上述可以看出,RBT虽然复杂,但是其操作是高效的。由于它本身是一种自平衡BST,所以它具有BST的所有性质。另外,RBT有自己特殊的性质,摘自《Introduction To Algorithms》:
<1>Every node is either red or black.
<2>The root is black.
<3>Every leaf (NIL) is black.
<4>If a node is red, then both its children are black.
<5>For each node, all simple paths from the node to descendant leaves contain the same number of black nodes.
从上述性质可以初步看出RBT跟BST最大的不同就是它多了一个颜色域,而这个颜色域非红即黑。
RBT的示意图如图5所示,来自《Introduction To Algorithms》插图:
图5 RBT示意图
其中黑色表示颜色为“Black”,灰色表示颜色为“Red”,T.nil表示的是sentinel(哨兵)结点,它的作用类似于BST中的NULL,目的是为了便于处理边界和节省空间,它表示所有的叶子结点和根结点的父结点。
首先给出头文件rbt.h:
#ifndef __RBT_H__
#define __RBT_H__
typedef enum __rbt_color {
RED,
BLACK
} rbt_color;
typedef struct __rbt_node {
int key;
struct __rbt_node *parent;
struct __rbt_node *left;
struct __rbt_node *right;
rbt_color color;
} rbt_node;
rbt_node *rbt_minimum(rbt_node *root);
rbt_node *rbt_maximun(rbt_node *root);
rbt_node *rbt_search(rbt_node *root, int key);
rbt_node *rbt_transplant(rbt_node **root, rbt_node *u, rbt_node *v);
void rbt_left_rotate(rbt_node **root, rbt_node *node);
void rbt_right_rotate(rbt_node **root, rbt_node *node);
int rbt_insert(rbt_node **root, int key);
void rbt_delete(rbt_node **root, rbt_node *del);
void rbt_preorder_walk(rbt_node *root);
void rbt_inorder_walk(rbt_node *root);
void rbt_postorder_walk(rbt_node *root);
#endif
以及sentinel的定义:
static rbt_node sentinel = {
0, // key
&sentinel, // parent
&sentinel, // left
&sentinel, // right
BLACK // color
};
2.1 旋转
RBT的插入和删除除了跟BST有相同的原理之外,还有额外的修正(fixup)操作,因为插入和删除操作可能会破坏RBT的5个性质中的一些性质。而修正操作的核心是旋转,分为左旋和右旋。
2.1.1 左旋
左旋和右旋示意图如图6所示,左旋是以x为支点,右旋以y为支点。
图6 左旋和右旋
左旋操作包括的步骤如图7所示:
<1>将支点(x)的right指针指向其右子结点(y)的左子结点,如果y的左子结点不为sentinel结点,那么将它的parent指针指向x。图7中红色虚线箭头所示。
<2>将y的parent指针指向x的parent(分为x为其父结点的左子结点或者右子结点两种情况)。图7中蓝色虚线箭头所示。
<3>将y的left指针指向x,x的parent指针指向y。图7中橙色虚线箭头所示。
图7 RBT左旋操作
左旋操作的代码实现如下:
/*
* 1st, deal with child pointer(sentinel or not)
* 2nd, deal with parent pointer(sentinel or not)
*/
void rbt_left_rotate(rbt_node **root, rbt_node *node)
{
rbt_node *y = node->right;
node->right = y->left; // turn y's left subtree into x's right subtree
if (y->left != &sentinel)
y->left->parent = node; // y's left child adopted to node
y->parent = node->parent; // link y's parent to node's parent
if (node->parent == &sentinel)
*root = y;
else if (node == node->parent->left)
node->parent->left = y;
else
node->parent->right = y;
y->left = node;
node->parent = y;
}
2.1.2 右旋
右旋与左旋是对称的,所以只需要将rbt_left_rotate中的left修改为right即可,这里图示和代码实现略。
2.2 插入操作
RBT的插入操作分为两个部分,第一部分跟BST的插入操作基本一样,第二部分是修正操作,因为插入结点有可能破坏RBT的性质。那么首先遇到的问题是插入的结点是什么颜色的呢?
<1>考虑插入的结点是黑色的,那么它只会且肯定破坏性质5,因为插入结点是插入到叶子结点位置,从任一结点开始,任意向下到叶子结点的路径上黑色结点数不再相等,因为插入结点的这一路径上的黑色结点数多了一个。
<2>考虑插入的结点是红色的,那么它可能破坏性质2或者4,因为当原树是空树的时候,插入一个红色结点就是根结点,而性质2指出它必须是黑色的;另外如果插入结点的父结点是红色的时候,性质4不满足。
那么插入结点是用什么颜色呢?采用<1>方法时,每次都会破坏性质5,所以每次都会进行fixup操作,采用<2>方法时,可能会破坏性质2或者4,但是可以看到,修复性质2的操作很简单,将红色修改为黑色即可;修复性质4虽然与修复性质5的复杂度差不多,但是性质4是可能被破坏的,而不是一定被破坏。
所以对比一下,插入结点时插入红色结点更好一些。插入结点操作的代码如下:
int rbt_insert(rbt_node **root, int key)
{
rbt_node *ptr = &sentinel, *new = NULL;
rbt_node *x = (*root == NULL) ? &sentinel : *root;
new = (rbt_node *)malloc(sizeof(rbt_node));
if (new == NULL) {
printf("malloc error!\n");
return -1;
}
// insert red node
new->key = key;
new->parent = &sentinel;
new->left = &sentinel;
new->right = &sentinel;
new->color = RED;
while (x != &sentinel) {
ptr = x;
if (new->key < x->key)
x = x->left;
else
x = x->right;
}
new->parent = ptr;
// root
if (ptr == &sentinel)
*root = new;
else {
if (new->key < ptr->key)
ptr->left = new;
else
ptr->right = new;
}
rbt_insert_fixup(root, new);
return 0;
}
从代码中可以看出,大多数的操作跟BST的插入操作差不多,只是将BST的NULL修改为RBT的sentinel结点了,并且多了一个修正操作rbt_insert_fixup。从上述已经知道插入结点为红色的时候可能破坏RBT的性质2或者性质4,性质2被破坏很好解决,关键是性质4被破坏了要怎么解决。假设插入的结点为z,其父结点为z.p,当经过变换后,如果z和z.p之间的颜色不都是红色时,那么修正就完毕了。《Introduction To Algorithms》中采用的方法是修改z.p的颜色,将颜色冲突向根部移动,并且穷举所有有冲突的可能性,经过旋转完成修正。所有的冲突有6种,但是由于对称性,实际上就简化为3种。插入结点修正操作时,主要的考察对象是z.p的兄弟,即z的叔叔(uncle)结点,下面只介绍z.p是z.p.p的左子结点的情况,z.p是z.p.p的右子结点的情况对称。注意:sentinel结点是黑色的。令y=z.p.p.right:
<a>z的叔叔结点是红色的:
图8 y的颜色是红色时的修正操作
这时将z.p的颜色染成黑色,性质4被修复,但是性质5被破坏,所以将y也染成黑色,再将z.p.p的颜色染成红色,性质5也被修复(这里没有理解,C结点不染成红色也没问题啊,仅猜测这么做是为了将颜色冲突向根结点移动),并以它为新的z,设为z',进入下一次修复操作,当z'是根结点时,只要把它染成黑色即可,如果不是根结点,那么操作跟上述类似。
<b>z的叔叔结点是黑色的,且z是z.p的右子结点
<c>z的叔叔结点是黑色的,且z是z.p的左子结点
图9 y的颜色是黑色时的修正操作
当z的叔叔结点是黑色的时候,两种情况以z是z.p的左子结点或者右子结点来区分。当z==z.p.right时,以z.p为支点左旋,成为另一种情况,这时没有解决颜色冲突,那么将z.p的颜色染成黑色,性质4被修复,但是性质5被破坏,所以将z.p.p染成红色,并且以它为支点右旋,性质5被修复。现在,z.p(即B结点)的父结点的染色不管是红色还是黑色都满足所有性质了,所以操作结束。
修正操作的代码:
/*
* Note: root may be modified.
* Why perform inserting red node rather than black node?
* 'cause inserting black node will always violate the 5th
* property, while inserting red node will probably violate
* the 2nd and the 4th properties. However, fixing up the
* 2nd property is so easy and the 4th properties may be
* not always violeated.
*/
static void rbt_insert_fixup(rbt_node **root, rbt_node *new)
{
rbt_node *y = &sentinel;
while(new->parent->color == RED) {
if (new->parent == new->parent->parent->left) {
y = new->parent->parent->right;
if (y->color == RED) {
new->parent->color = BLACK;
y->color = BLACK;
new->parent->parent->color = RED;
new = new->parent->parent;
} else {
if (new == new->parent->right) {
new = new->parent;
rbt_left_rotate(root, new);
}
new->parent->color = BLACK;
new->parent->parent->color = RED;
rbt_right_rotate(root, new->parent->parent);
}
} else { /* symmetric to left */
y = new->parent->parent->left;
if (y->color == RED) {
new->parent->color = BLACK;
y->color = BLACK;
new->parent->parent->color = RED;
new = new->parent->parent;
} else {
if (new == new->parent->left) {
new = new->parent;
rbt_right_rotate(root, new);
}
new->parent->color = BLACK;
new->parent->parent->color = RED;
rbt_left_rotate(root, new->parent->parent);
}
}
}
(*root)->color = BLACK;
}
因为修正操作不涉及被外部接口调用,所以定义为static的。
2.3 删除操作
RBT的删除操作跟它的插入操作一样分为两部分,第一部分是删除结点操作,跟BST的结点删除操作类似,但是由于删除结点后可能会破坏某些性质,所以也要进行修正操作。
由于删除操作事先是不知道要删除的结点的颜色的,如果要删除的结点是红色的,那么RBT的性质不会被破坏,但是如果要删除的结点是黑色的,那么RBT的性质2,性质4或者性质5有可能被破坏。
删除结点操作的代码如下:
/*
* Deleting red code will not violate properties.
*/
void rbt_delete(rbt_node **root, rbt_node *del)
{
/*
* x keeps track of the node moves
* into y's original position
*/
rbt_node *y = del, *x = NULL, *r = NULL;
rbt_color y_original_color = y->color;
if (del->left == &sentinel) {
x = del->right;
r = rbt_transplant(root, del, del->right);
rbt_node_free(&r);
} else if (del->right == &sentinel) {
x = del->left;
r = rbt_transplant(root, del, del->left);
rbt_node_free(&r);
} else {
// search for mini-key node in the right subtree
y = rbt_minimum(del->right);
y_original_color = y->color;
x = y->right;
// mini-key node is child of the to-be-deleted node
if (y->parent == del)
x->parent = y;
else {
/*
* do not need to free y 'cause del will be replaced with it
*/
rbt_transplant(root, y, y->right);
y->right = del->right; // move y to the to-be-deleted node
y->right->parent = y; // the original right child reparented to y
}
r = rbt_transplant(root, del, y);
y->left = del->left; // y's left child now is the original left child
y->left->parent = y; // the original left child reparented to y
y->color = del->color;
rbt_node_free(&r);
}
if (y_original_color == BLACK)
rbt_delete_fixup(root, x);
}
RBT删除操作的第一部分跟BST的删除操作类似,y_original_color保存了要删除的结点的颜色,如果它是红色的,不执行修正操作,因为没有性质被破坏;如果它是黑色的,那么就要执行修正操作了。因为RBT有sentinel结点,所以它的移植操作跟BST的稍微有点差异:
rbt_node *rbt_transplant(rbt_node **root, rbt_node *u, rbt_node *v)
{
rbt_node *d = NULL;
// child
if (u->parent == &sentinel) {
//u is root itself
d = *root;
*root = v;
} else {
d = u;
if (u == u->parent->left)
u->parent->left = v;
else
u->parent->right = v;
}
// parent
v->parent = u->parent;
return d;
}
具体是NULL被sentinel取代,且v->parent = u->parent;前面不需要判断,因为sentinel结点肯定不为空。
为了在修正过程中能保持性质5,可以假设取代要删除的结点的结点颜色有另外一层黑色(可以理解为它从被删除结点那儿继承来的),那么性质5没有被破坏,当取代的结点的颜色为红色时,那么取代的结点的颜色为“一红一黑”,为了不破坏性质1,它表示红色;如果取代的结点的颜色为黑色时,那么取代的结点的颜色为“双黑”,为了不破坏性质1,它表示黑色。前后有点矛盾,只用理解为外加的一层黑色是想象的,只是为了维持性质5,而非真的是两重色,在操作取代结点时,仍然以它本身的颜色为准。
修正操作的参考结点是取代结点的兄弟结点和侄子结点。修正操作有8种情况,但是由于对称性,实际上只有4种情况,这里只以取代结点(x)是其父结点的左子结点为例:
图10 删除结点后的修正操作情况
<a>取代结点的兄弟结点是红色的。
<b>取代结点的兄弟结点是黑色的,且其两个侄子结点都是黑色的。
<c>取代结点的兄弟结点是黑色的,且其左侄子结点颜色是红色的,其右侄子结点颜色是黑色的。
<d>取代结点的兄弟结点是黑色的,且其右侄子结点颜色是红色的(左子结点颜色任意)。
注意:图中黑色表示“Black”,灰色表示“Red”,白色表示“Red or Black”。由于取代结点和其兄弟结点的颜色都是黑色的时候,无法判断它们的父结点是什么颜色,因为在删除结点之前,它们的父结点的颜色不管是红色还是黑色,都满足RBT的性质。
对于<a>情况,将取代结点(x,下同)的兄弟结点染成黑色,将其父结点染成红色,再以x的父结点为支点左旋。最后将新的参考结点指向x的兄弟结点,即x的兄弟结点成为黑色结点了,可以进入情况<b>,<c>和<d>。
对于<b>情况,将x的兄弟结点染成红色,并将参考结点指向其父结点,将颜色冲突向根部移动。
对于<c>情况,将x的左侄子结点染成黑色,将x的兄弟结点染成红色,以x的兄弟结点为支点右旋,并将参考结点指向x的兄弟结点。
对于<d>情况,将x的右侄子结点染成黑色,将x的兄弟结点染成其父结点的颜色,再将其父结点染成黑色,以其父结点为支点左旋,然后将新的x指向T.root。重点讲下这个:因为x这条路径删除了个黑色结点,那么这条路径上就少了个黑色结点,破坏性质5,将x的兄弟结点染成其父结点的颜色,且把其父结点和右侄子结点染成黑色,再进行左旋,那么x原来的兄弟结点的路径上黑色结点没有改变(x的右侄子结点染成黑色),x所在的路径多了一个黑色的结点(x的父结点染成黑色,并左旋到x的路径上),所以两条路径上的黑色结点数相同了。
与插入操作的修正操作一样,不涉及被外部调用,所以定义为static,下面是修正操作的代码:
/*
* Note: when root is to be deleted, its pointer will be modified.
* Deleting red node will not violate properties.
*/
static void rbt_delete_fixup(rbt_node **root, rbt_node *node)
{
rbt_node *w = &sentinel;
while (node != *root && node->color == BLACK) {
if (node == node->parent->left) {
w = node->parent->right;
// case 1
if (w->color == RED) {
w->color = BLACK;
node->parent->color = RED;
rbt_left_rotate(root, node->parent);
w = node->parent->right;
}
// case 2
if (w->left->color == BLACK && w->right->color == BLACK) {
/* case 2 --> case 1
* now w's color is not cleared
*/
w->color = RED;
node = node->parent;
} else {
// case 3
if (w->right->color == BLACK) {
w->left->color = BLACK;
w->color = RED;
rbt_right_rotate(root, w);
w = node->parent->right;
}
// case 4
w->color = node->parent->color;
w->right->color = BLACK;
node->parent->color = BLACK;
rbt_left_rotate(root, node->parent);
node = *root; // what the fuck?
}
} else {
w = node->parent->left;
// case 1
if (w->color == RED) {
w->color = BLACK;
node->parent->color = RED;
rbt_right_rotate(root, node->parent);
w = node->parent->left;
}
// case 2
if (w->left->color == BLACK && w->right->color == BLACK) {
/* case 2 --> case 1
* now w's color is not cleared
*/
w->color = RED;
node = node->parent;
} else {
// case 3
if (w->left->color == BLACK) {
w->right->color = BLACK;
w->color = RED;
rbt_left_rotate(root, w);
w = node->parent->left;
}
// case 4
w->color = node->parent->color;
w->left->color = BLACK;
node->parent->color = BLACK;
rbt_right_rotate(root, node->parent);
node = *root;
}
}
}
node->color = BLACK;
}
到此为止,红黑树的各种操作的C实现就基本结束了,其中序遍历代码如下:
void rbt_inorder_walk(rbt_node *root)
{
rbt_node *y = root;
if (y != &sentinel) {
rbt_inorder_walk(y->left);
if (y->color == RED)
printf("<RED ");
else
printf("<BLACK ");
printf(" key: %d> ", y->key);
rbt_inorder_walk(y->right);
}
}
验证时用到了下列网址的示意图,在此表示感谢:
http://saturnman.blog.163.com/blog/static/557611201097221570/
根据示意图,结合插入和删除的遍历打印,已经验证代码可以正确执行,但未考虑执行效率。