二 紅黑樹(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/
根據示意圖,結合插入和刪除的遍歷打印,已經驗證代碼可以正確執行,但未考慮執行效率。