紅黑樹原理和實現(1)

       曾經看Robert Love寫的《Linux Kernel Development》第三版中曾提到過Linux內核的程序調度算法是用紅黑樹(Red-Black Tree,下文簡稱RBT)實現的,當時找了很多關於RBT的東西,包括博客,數據結構和算法書籍甚至動態圖等。由於唸書的時候唸的不是計算機相關專業,沒有數據結構特別是樹的基礎,根本看不懂。工作兩年多之後,由於工作需要接觸了nginx,其中的超時部分也是用RBT實現的。所以覺得有必要把它弄明白。爲此寫博客記下自己的學習過程,以後忘了好馬上找到!

       要理解RBT首先要理解二叉查找樹(Binary Search Tree,下文簡稱BST),因爲RBT也是BST。BST的基本操作包括插入操作,刪除操作,遍歷,查找,後繼結點查找,前驅結點查找(一般BST刪除操作中如果該結點有兩個子結點,那麼一般找它的後繼結點,這是默認做法,當然也可以找它的前驅結點;注意,這裏的前驅結點和後繼結點是以中序遍歷的結點順序來說的)和移植(transplant)操作。RBT在這些操作的基礎上還要加上左旋(left rotate)和右旋(right rotate)操作,這些操作是在進行插入和刪除操作(與BST的一樣)後進行修正的操作,以維持RBT的性質。下面首先簡要介紹BST的一些知識。

一 二叉搜索樹(Binary Search Tree)

       BST又稱二叉排序樹(Sorted(Ordered) Binary Tree),它具有如下的性質:從根結點起,其左子樹的所有結點的值(Key)都小於它的值,其右子樹的所有結點的值都大於它的值,並且對於子樹中的任意非葉子結點也有同樣的性質。BST示意圖如圖1所示,來自維基百科。

圖 1 BST示意圖

       下面給出我自己參考《Introduction To Algorithms》第三版僞代碼後自己實現的代碼(C語言實現,我以前聽人說數據結構用C++實現是首選,當時不理解,但是看過BST和RBT之後我覺得C++實現是比C更加方便。大概思路:RBT類繼承BST類,再添加額外的幾個操作,使用模板處理不同結構類型。但是C++基礎不太紮實,就沒用C++實現)。首先給出頭文件bst.h,如下所示:

#ifndef __BST_H__
#define __BST_H__

typedef struct __bst_node {
	int key;
	struct __bst_node *parent;
	struct __bst_node *left;
	struct __bst_node *right;
}bst_node;

bst_node *bst_minimum(bst_node *node);
bst_node *bst_maximun(bst_node *node);
bst_node *bst_search(bst_node *root, int key);
bst_node *bst_transplant(bst_node **root, bst_node *u, bst_node *v);
int bst_insert(bst_node **root, int key);
void bst_delete(bst_node **root, bst_node *del);
void bst_preorder_walk(bst_node *root);
void bst_inorder_walk(bst_node *root);
void bst_postorder_walk(bst_node *root);

#endif

1.1 插入操作

       插入操作是就是插入葉子結點(除了根結點之外):

int bst_insert(bst_node **root, int key)
{
	bst_node *ptr = NULL, *new = NULL;
	bst_node *x = *root;

	new = (bst_node *)malloc(sizeof(bst_node));
	if (new == NULL) {
		printf("malloc error!\n");
		return -1;
	}

	new->key = key;
	new->parent = NULL;
	new->left = NULL;
	new->right = NULL;

	while (x != NULL) {
		ptr = x;
		if (new->key < x->key)
			x = x->left;
		else
			x = x->right;
	}

	// root
	if (ptr == NULL)
		*root = new;
	else {
		new->parent = ptr;
		if (new->key < ptr->key)
			ptr->left = new;
		else
			ptr->right = new;
	}

	return 0;
}
       簡要說明一下:每個結點的結構體包括整形變量key,parent指針,left指針和right指針。

       由於根結點最開始指向NULL,所以採用指針的指針的形式(單指針不能回傳地址修改,詳見我的博客中C語言指針總結的相關內容)。

       在函數體內分配新的結點的內存空間。

       如果是根結點,那麼將它指向*root,否則與當前結點的值比較,如果小於當前結點的值,那麼插入的結點應該在當前結點的左子樹中,否則應該在右子樹中。

       最後,噹噹前結點已經是葉子結點,那麼就可以進行插入操作了,當插入結點的值比當前結點的值小,那麼插入結點就作爲當前結點的左子結點插入,否則作爲當前結點的右子結點插入。

       注意:插入操作容易出錯的地方是對parent指針,left指針和right指針的修改,包括插入結點的parent指針修改爲指向已存在樹的相關結點和已存在樹的相關結點的left指針或者right指針修改爲指向插入結點。如下圖所示:

圖2 BST插入結點示意圖

       上圖是根據圖1畫出的,原圖中沒有畫出指針的指向關係,這裏畫出是爲了便於理解實現的細節。

1.2 刪除操作

       刪除操作相對於插入操作來說複雜一些,刪除操作分爲三種情況:

       <1>要刪除的結點沒有子結點,即它爲葉子結點或者根結點,只需刪除即可。

       <2>要刪除的結點只有一個子結點,首先用它的子結點替換它,然後再將其刪除。

       <3>要刪除的結點有兩個子結點,這時爲了維持BST的性質,首先要從其左子樹中找到包含最大值的結點或者從其右子樹中找到包含最小值的結點(一般用後者替換要刪除的結點),然後替換它,最後將其刪除。

       情況<1>是最簡單的,情況<2>和<3>需要額外的一些操作,如移植(就是<2>和<3>中提到的替換)和查找子樹中包含最大值(最小值)的結點。

1.2.1 查找右子樹中包含最小值的結點,即後繼結點(左子樹中包含最大值的結點,即前驅結點略)

       假設某個結點存在右子樹,那麼該右子樹中包含最小值的結點存在於該右子樹的左子樹,且爲最左邊的那個結點。如圖1中的10是根節點在右子樹中包含最小值的結點,爲什麼不是13,它“看起來纔是最左邊”的結點,因爲它不在根節點的右子樹的左子樹中,而是在根結點的右子樹的右子樹中。實現代碼如下:

bst_node *bst_minimum(bst_node *node)
{
    while (node->left != NULL)
        node = node->left;

    return node;
}

1.2.2 移植

       當要刪除的結點有至少一個結點時,會涉及到移植。如下圖所示,假設要移植的結點分別是10和14:

圖3 BST移植結點示意圖

       其中虛線是需要執行的操作,如果10的子結點是它的左孩子,操作也類似。實現代碼如下,其中考慮了要移植的結點是根結點的情況:

bst_node *bst_transplant(bst_node **root, bst_node *u, bst_node *v)
{
	bst_node *d = NULL;
	// child
	if (u->parent == NULL) {
		//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
	if (v != NULL)
		v->parent = u->parent;

	return d;
}

       上面代碼中的指針d的作用是返回指向被移植結點的地址,從下面的刪除操作代碼中可以看出來。刪除操作的示意圖如圖4所示,當要刪除的結點有兩個子結點時,又分爲兩種情況:

       <1>要刪除的結點的後繼結點就是它的右孩子(爲什麼不是左孩子,因爲我們選擇的是後繼結點,不是前驅結點)

       <2>要刪除的結點的後繼結點不是它的右孩子

                                                            (a)                                                                                                                                                              (b)

圖4 BST刪除結點示意圖

       圖4中(a)情況表示要刪除的結點是8,它的後繼結點就是它的右孩子,那麼直接將它替換根結點,並且將原根結點的左孩子指向它的後繼結點。

       圖4中(b)情況表示要刪除的結點是3,這裏畫得有點複雜,爲了更全面表示刪除操作,在圖1的基礎上添加了一個結點5,它是結點4的右孩子。結點4是結點3的後繼結點。執行的操作如下:

       <1>移植結點5爲結點6的左孩子,如圖中紅色虛線箭頭所示,即結點5替換了結點4原來的位置

       <2>將結點3的右孩子“過繼”給結點4,如圖中藍色虛線箭頭所示

       <3>將結點4替換結點3,如圖中綠色虛線箭頭所示

       <4>將結點3的左孩子“過繼”給結點4,如圖中橙色虛線箭頭所示

       下面給出刪除結點的代碼實現:

/*
 * note: when root is to be deleted, its pointer will be modified
 */
void bst_delete(bst_node **root, bst_node *del)
{
	bst_node *y = NULL, *r = NULL;

	if (del->left == NULL) {
		r = bst_transplant(root, del, del->right);
		bst_node_free(&r);
	} else if (del->right == NULL) {
		r = bst_transplant(root, del, del->left);
		bst_node_free(&r);
	} else {
		// search for mini-key node in the right subtree
		y = bst_minimum(del->right);

		// mini-key node is not child of the to-be-deleted node
		if (y->parent != del) {
			/*
			 * do not need to free y 'cause del will be replaced with it
			 */
			bst_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 = bst_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
		bst_node_free(&r);
	}
}

1.3 一些額外的操作

       從上面的代碼中可以看出,上文中bst_transplant函數返回的結點有可能是要刪除的結點,bst_node_free函數是釋放結點操作,它的定義是:

static void bst_node_free(bst_node **node)                                                                                                              
{
    (*node)->parent = NULL;
    (*node)->left = NULL;
    (*node)->right = NULL;
    free(*node);
    *node = NULL;
}

       最後給出BST中序遍歷的代碼作爲BST算法實現的結束,前序遍歷和後序遍歷的代碼略。下一篇將介紹RBT的算法實現,可以從下一篇RBT算法中實現中找到很多本文代碼的影子,因爲是用C實現的,所以代碼重複的地方比較多。

void bst_inorder_walk(bst_node *root)
{
    bst_node *y = root;
     
    if (y != NULL) {
        bst_inorder_walk(y->left);
        printf("%d ", y->key);
        bst_inorder_walk(y->right);
    }   
}


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章