紅黑樹原理和實現(2)

二 紅黑樹(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/

       根據示意圖,結合插入和刪除的遍歷打印,已經驗證代碼可以正確執行,但未考慮執行效率。

發佈了22 篇原創文章 · 獲贊 20 · 訪問量 10萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章