非遞歸學習樹結構(六)--RB-Tree(紅黑樹)



終於深入的理解並用代碼一步一步實現了紅黑樹。Linux內核的進程調度和內存管理,STL庫的map容器的實現都使用了紅黑樹,這是一種效率很高的二叉搜索樹,效率基本上滿足log(n),它與AVL樹有什麼區別呢?


學習了AVL樹之後,如果你覺得關於二叉搜索樹的學習就結束了,那你就錯了,因爲,還有一個樹,紅黑樹,紅黑樹學習完之後,二叉搜索樹基本上就告一段落了,還有一些二叉搜索樹的變種,如AA-Tree,在這裏就不講了,弄明白紅黑樹,學習AA樹應該也不是太難的事情了。


可能有人要問,有了AVL樹,幹嘛還要紅黑樹呢?是啊,AVL樹效率是最穩定最高的二叉搜索樹,因爲AVL是一顆高度平衡的二叉樹,任意一個結點的左右子節點高度差不大於1,有人問,有沒有高度差爲0的,那我想請問你,給你兩個點,你給我造出來一個高度差爲0的樹出來!!!


閒話不多扯,還是說說AVL樹和RB-Tree的故事。AVL樹是高度平衡,爲了達到高度平衡,在建樹和刪除的時候操作較爲複雜,並不是說變化的算法複雜,而是牽扯的結點較多,有時候爲了在插入一個結點以後能滿足AVL樹的性質,整棵樹都會跟着旋轉變換。插入和刪除的效率較低,而RB-Tree就是爲了在提高插入刪除效率的同時,又不會使查找效率有大的變化而誕生的。紅黑樹不要求高度平衡,但是其也是一顆泛平衡樹,因爲從根節點出發,到葉子節點的最長距離不會超過最短距離的二倍,從下面紅黑樹的性質就可以看出來:


1.Anode is either red or black.

一個結點非黑即紅。

2.Theroot is black.

根節點是黑色

3.Allleaves (NIL) are black.

所有葉子節點是黑色

4.Ifa node is red, then both its children are black.

如果一個結點是紅色,則它的孩子節點必須是黑色。

5.Everypath from a given node to any of its descendant NIL nodes contains the samenumber of black nodes.The uniform number of blacknodes in the paths from root to leaves is called the black-height of thered–black tree.

黑高相同。

 

所謂的黑高,就是指從一個節點出發,到葉子節點(NIL)所經歷的黑色節點的個數

如下圖所示





這裏我們引入了一個NIL節點,這個NIL結點的顏色是黑色的,它並不是數據,而是作爲類似於標記用的,說明已經到底了,爲什麼引入這個NIL節點?是爲了簡化刪除中的調整代碼,在刪除的調整函數void deleteadjust(Node** root, Node* node)的註釋中會有說明。


 


紅黑樹的難點在於插入和刪除的調整上,插入和刪除節點以後都要保證不會破壞上述五點紅黑樹特性。插入操作比較簡單一些,刪除操作比較複雜一點,但是,操作的思路還是比較清晰的,下面我們就結合圖例還說明插入和刪除的各種case,以及對應的操作方法。


插入:


紅黑樹規定:待插入的結點的顏色都要定義成紅色。


插入操作我們分爲兩部分,一部分是插入,另一部分是插入後的調整。插入操作和前面BST樹以及AVL樹的插入一樣。插入完畢後,有可能遇到以下幾種情況:


Case 1




上圖中N是插入的結點,可以看到,N和其父節點P都是紅色,而且N的叔結點U也是紅色,這就違背了性質4如果一個結點是紅色,則它的孩子節點必須是黑色),調整的操作方法是:將PU都變爲黑色,G變爲紅色,這樣就滿足了性質4,也不會違背性質5,完美結束。



Case 2




N結點的叔結點U如果是黑節點,操作:G改變爲紅色,P改變爲黑色,在P出一次右旋,就OK了。


 


Case 3


3


可以看到,圖2和圖1有點像,只要對P進行一次左旋,左旋之後就變成了case2的情況,按照case的操作,就OK了。


後面的插入操作的代碼就可以照着上圖進行理解,因爲插入操作比較簡單,因此沒有過多的說明。本文的重點放在了刪除操作上,插入操作還可以結合以下兩個鏈接的內容來理解:


https://en.wikipedia.org/wiki/Red%E2%80%93black_tree


http://www.cnblogs.com/fornever/archive/2011/12/02/2270692.html(可以理解是維基百科的中文翻譯,英文好的同學就不必看了)


 


刪除:


下面,我們就重點說說紅黑樹的刪除操作,


BST以及AVL樹的刪除操作一樣,其實是找到要刪除節點的直接後繼(右子樹的最左子節點)或者直接前驅節點(左子樹的最右子節點),然後用直接後繼(或者直接前驅,後面我們都以直接後繼來描述)的值替換到要刪除的結點的值,然後刪除直接後繼節點。


如果這個直接後繼節點是紅色,而且孩子節點都是NIL,那就省事了,直接刪除就行了,因爲刪除這個紅節點不會破壞任何一個性質。


如果這個直接後繼節點N是黑色,而他的右孩子SR是紅色,那麼SR的兩個孩子肯定是NIL,因爲SR本身是紅色,隨意本身不可能跟紅色節點,而N的左節點是nil,這樣就是的SR不能有黑色節點,否則就會使得N的左右子樹的黑高不一樣。調整操作是把SR染成黑色,替換掉N節點即可。


上面我們討論了兩種比較簡單的情況,那麼我們處理一下負責的情況,那就是如果N是一個黑色的結點,而且N的兩個孩子都是NIL節點,那麼刪除了N以後就會破壞平衡,如下圖。





刪除了N以後,P的左子樹的黑高是少1的,這是就需要調平,當然,P的右子節點不一定是黑色的,這裏只是畫出一種情況進行說明。後面涉及到的各種case,其實就是P和其右子節點顏色各種不同造成的,下面就一一分析。


case 1:


case 2:


case 3:

case 4:

case 5:

case 5的圖只畫出了P節點右子樹,N節點是P的左子樹,沒有畫,請自行腦補即可。


以上刪除操作都是討論的N是P的左子節點,S是P的右子節點的情況,至於N是P右子節點,S是P左子節點的情況,操作一樣,只是左旋變右旋,右旋變左旋以及操作的子節點不一樣,操作方法一樣。



上面五種case就是能遇到的情況。這裏說一下爲什麼增加NIL這個節點,設想一下,如果沒有NIL這個節點而是用NULL,那麼我們在第一次進行調整的時候就得作爲特殊情況考慮,因爲NULL是沒有color的,然後才能用通用的調整代碼,這裏我們引入了NIL之後就好多了,NIL本身無數據,但是其有顏色,所以能將第一次調整操作也融入到通用代碼中了。


好了,下面我們就把代碼貼上:

/*RB-Tree.c*/
#include "RB-Tree.h"
static Node* NIL = NULL;

/*get grandparent*/
Node* grandparent(Node* node)
{
	return node->pp->pp;
}

/*get parent*/
Node* parent(Node* node)
{
	return node->pp;
}

/*get uncle*/
Node* uncle(Node* node)
{
	if (grandparent(node) == NULL) {
		return NULL;
	}
	return (parent(node) == grandparent(node)->plc) ? grandparent(node)->prc : grandparent(node)->plc;
}

Node* createnode(int key,int val)
{
	Node* node = (Node*)malloc(sizeof(Node));
	node->cnt = 0;
	color = RED;
	node->color = color;
	node->key = key;
	node->val = val;
	node->plc = NIL;
	node->prc = NIL;
	node->pp = NIL;

	return node;
}

/*左旋轉*/
Node* Left_Rotate(Node** root, Node* node)
{
	Node* tmp = node->prc;
	node->prc = tmp->plc;
	if (tmp->plc != NULL) {
		tmp->plc->pp = node;
	}

	tmp->pp = parent(node);
	if (parent(node) == NULL) {
		*root = tmp;
	}
	else {
		if (parent(node)->plc == node) {
			parent(node)->plc = tmp;
		}
		else {
			parent(node)->prc = tmp;
		}
	}
	node->pp = tmp;
	tmp->plc = node;

	return tmp;
}

/*右旋轉*/ 
Node* Right_Rotate(Node** root, Node* node)
{
	Node* tmp = node->plc;
	node->plc = tmp->prc;
	if (tmp->prc != NULL) {
		tmp->prc->pp = node;
	}

	tmp->pp = node->pp;

	if (parent(node) == NULL) {
		*root = tmp;
	}
	else {
		if (node->pp->plc = node) {
			node->pp->plc = tmp;
		}
		else {
			node->pp->prc = tmp;
		}
	}
	tmp->prc = node;
	node->pp = tmp;

	return tmp;
}

/*插入過程*/
RESULT _insert_(Node** root, Node* node)
{
	Node* cur = *root;
	Node* tmp = NULL;

	while (cur != NIL) 
	{
		tmp = cur;
		if ( node->val > cur->val ) {
			cur = cur->prc;
		}
		else if (node->val < tmp->val) {
			cur = cur->plc;
		}
		else {
			cur->cnt++;
			return 2;
		}
	}

	node->pp = tmp;
	if (node->key < tmp->key) {
		tmp->plc = node;
	}
	else {
		tmp->prc = node;
	}

	return SUCCESS;
}

/*插入節點後的調整函數*/
void insertadjust(Node**root,Node* node)
{
	while (parent(node)->color == RED)
	{
		/*這裏分兩種情況,叔結點是紅色,叔結點不是紅色(叔結點是NULL或者叔結點是黑色)*/
		/*叔結點是紅色*/
		if (uncle(node) != NIL && uncle(node)->color == RED) {
			parent(node)->color = BLACK;
			uncle(node)->color = BLACK;
			grandparent(node)->color = RED;
			node = grandparent(node);
			if (parent(node) == NULL)
			{
				break;
			}
		}
		/*叔結點是空或者叔結點是黑色*/
		else {
			if (parent(node) == grandparent(node)->plc) {
				if (node == parent(node)->prc) {
					/*左旋*/
					node = Left_Rotate(root, parent(node))->plc;
				}
				parent(node)->color = BLACK;
				grandparent(node)->color = RED;
				/*右旋*/
				Right_Rotate(root, grandparent(node));
			}
			else {
				if (node == parent(node)->plc) {
					/*右旋*/
					node = Right_Rotate(root, parent(node))->prc;
				}
				parent(node)->color = BLACK;
				grandparent(node)->color = RED;
				/*左旋*/
				Left_Rotate(root, grandparent(node));
			}
			break;
		}
	}
}

/*插入節點,並進行必要的調整*/
RESULT insert(Node** root, int key, int val)
{
	Node* node = NULL;
	
	if (*root == NULL) {
		NIL = (Node*)malloc(sizeof(Node));
		*root = (Node*)malloc(sizeof(Node));
		NIL->color = BLACK;
		(*root)->color = BLACK;
		(*root)->plc = NIL;
		(*root)->prc = NIL;
		(*root)->pp = NULL;
		(*root)->key = key;
		(*root)->val = val;
		(*root)->cnt = 1;
		return SUCCESS;
	}
	else 
	{
		node = createnode(key, val);
		if (!node) {
			perror("malloc failure!!!\n");
			return FAILURE;
		}
	}
	/*插入節點*/
	if (2 == _insert_(root, node)) /*返回2說明插入的值已經存在,只需要計數變量+1即可*/ {
		return SUCCESS;
	}

	/*父節點如果是黑色,就不會衝突,不會違背任何性質,如果父節點是紅色,則需要調整*/
	insertadjust(root, node);

	(*root)->color = BLACK;
	return SUCCESS;
}

Node* search(Node * root, int key)
{
	Node* node = root;
	while (node != NULL)
	{
		if (key > node->key) {
			node = node->prc;
		}
		else if (key < node->key) {
			node = node->plc;
		}
		else
			break;
	}
	return node;
}


void deleteadjust(Node** root, Node* node)
{
	/*nil節點的用處體現在此處,剛進入此函數是,node是nil節點,這樣纔不會報錯,不然如果沒有nil,而是NULL,
	此處肯定是要報錯的(node->color),如果不使用nil節點而用null,要想跳過這個錯誤,只能在這個函數外
	先進行一次處理,使得node不在是NULL以後才能進入此函數*/
	while (node != *root && node->color == BLACK)
	{
		/*node節點經過迭代之後,就不能確定其實其父節點的左子節點還是右子節點了,因此分兩種情況處理,
		兩種情況下,旋轉方向不同,因此其兄弟節點的左右子節點顏色判斷要相反,case 4 和 case 5 體現了這一點*/
		if (node == parent(node)->plc) {
			Node* Parent = parent(node);
			Node* Brother = parent(node)->prc;/*默認取右子樹的最左子節點,所以兄弟節點是右孩子*/

			if (Brother->color == RED) {
				/*case 1:兄弟節點brother紅色,父節點P,brother的左右孩子(不爲NIL,否則原來就不平衡)一定是黑,
				操作:Parent和Brother的顏色互換,在Parent點左旋轉,現在P的左子樹還是比右子樹黑高少1,因此,
				將Brother更新一下,在node處重新進行判斷調整*/
				Brother->color = BLACK;
				Parent->color = RED;
				Left_Rotate(root,parent(node));
				Brother = Parent->prc;
			}
			else if (Parent->color == BLACK && Brother->color == BLACK &&
				Brother->plc->color == BLACK && Brother->prc->color == BLACK) {
				/*case 2:Parent節點是黑色,Brother節點以及Brother節點的左右子節點都是黑色,
				操作:將Brother節點變成紅色,在Parent節點處左旋,相當於原來以P節點(現在是以B節點)爲根子節點
				的子樹黑高整體減少1,用Brother節點代替原來的node節點,重新進行判斷調整*/
				Brother->color = RED;
				Left_Rotate(root,Parent);
				node = Brother;
				continue;
			}
			else if (Parent->color == RED &&
				Brother->plc->color == BLACK && Brother->prc->color == BLACK) {
				/*case 3:Parent是紅色,Brother的孩子們都是黑色
				操作:將Parent變爲黑色,Brother變爲紅色即可滿足性質*/
				Parent->color = BLACK;
				Brother->color = RED;
				node = *root;
			}
			else if (Brother->color == BLACK && Brother->prc->color == RED) {
				/*case 4:Parent爲任意色,如果Brother的右子節點是紅色,左子節點顏色任意
				Brother顏色編程parent的顏色,Parent的顏色變爲黑色,Brother右子節點的顏色改爲黑色*/
				Brother->color = Parent->color;
				Parent->color = BLACK;
				Brother->prc->color = BLACK;
				Left_Rotate(root,Parent);
				node = *root;
			}
			else if (Brother->color == BLACK &&
				Brother->plc->color == RED && Brother->prc->color == BLACK) {
				/*case 5:Parent顏色任意,Brother顏色是黑色,Brother的左子節點是紅色,右孩子是黑色,
				操作:B的左子節點變爲黑色,B變爲紅色,然後在B出右旋,處理以後就變成了case 4的情況*/
				Brother->plc->color = BLACK;
				Brother->color = RED;
				Right_Rotate(root,Brother);
				Brother = Brother->plc;/*Brother的左子節點代替了原來brother的位置,因此更新Brother的指向*/
				/*對於case 5 處理一下之後變成了case 4的情況,在case 4處理跳出*/
			}
		}
		else {
			Node* Parent = parent(node);
			Node* Brother = parent(node)->plc;/*默認取右子樹的最左子節點,所以兄弟節點是右孩子*/

			if (Brother->color == RED) {
				/*case 1*/
				Brother->color = BLACK;
				Parent->color = RED;
				Right_Rotate(root, parent(node));
				Brother = Parent->plc;
			}
			else if (Parent->color == BLACK && Brother->color == BLACK &&
				Brother->plc->color == BLACK && Brother->prc->color == BLACK) {
				/*case 2*/
				Brother->color = RED;
				Right_Rotate(root, Parent);
				node = Brother;
				continue;
			}
			else if (Parent->color == RED &&
				Brother->plc->color == BLACK && Brother->prc->color == BLACK) {
				/*case 3*/
				Parent->color = BLACK;
				Brother->color = RED;
				node = *root;
			}
			else if (Brother->color == BLACK && Brother->plc->color == RED) {
				/*case 4*/
				Brother->color = Parent->color;
				Parent->color = BLACK;
				Brother->plc->color = BLACK;
				Right_Rotate(root, Parent);
				node = *root;
			}
			else if (Brother->color == BLACK &&
				Brother->prc->color == RED && Brother->plc->color == BLACK) {
				/*case 5*/
				Brother->prc->color = BLACK;
				Brother->color = RED;
				Left_Rotate(root, Brother);
				Brother = Brother->prc;
			}
		}
	}
	node->color = BLACK;
}

void _delete_(Node** root, Node* node)
{
	Node* tmp = NULL;/*實際要被刪除的點*/

	if (node->plc == NIL || node->prc == NIL) {
		tmp = node;
	}
	else {
		Node* rightsubtree = node->prc;
		Node* minright = rightsubtree;

		while (rightsubtree->plc != NIL) {
			minright = rightsubtree->plc;
			rightsubtree = rightsubtree->plc;
		}
		tmp = minright;
	}
	Node* s = NULL;/*頂替實際要被刪除的點的子節點*/
	if (tmp->plc != NIL) {
		s = tmp->plc;
	}
	else {
		s = tmp->prc;
	}

	s->pp = parent(tmp);
	if (tmp->pp == NIL) {
		*root = s;
	}
	else {
		if (tmp == parent(tmp)->plc) {
			parent(tmp)->plc = s;
		}
		else {
			parent(tmp)->prc = s;
		}
	}

	if (tmp != s) {
		node->key = tmp->key;
		node->val = tmp->val;
	}
	/*實際要刪除的點的顏色,如果是紅色,刪除之後不影響,如果是黑色,則需要調整*/
	if (BLACK == tmp->color) {
		if (s->color == RED) { /*實際被刪除的點的子節點如果是紅色,直接將其變爲黑色,即滿足性質*/
			s->color = BLACK;
		}
		else {
			deleteadjust(root, s);
		}
	}
}

/*刪除指定key的元素*/
RESULT delete(Node** root, int key)
{
	Node* node = search(*root, key);

	if (node == NIL)
	{
		return SUCCESS;
	}

	/*如果實際刪除的點是個黑色的,就需要調整,不然違背紅黑樹性質*/
	_delete_(root,node);

	return SUCCESS;
}

測試代碼:

/*test.c*/
#include "RB-Tree.h"

int main()
{
	Node* root = NULL;
	//int elemarry[] = { 16, 12, 18, 10, 14, 17, 20, 9, 11, 13, 15, 8 };
	int elemarry[] = { 1, 6, 11, 13, 15, 17, 22, 25,27 };

	for (int i = 0; i < sizeof(elemarry) / sizeof(int); i++)
	{
		if (SUCCESS != insert(&root, elemarry[i], elemarry[i]))
		{
			perror("insert failure!!!\n");
			return FAILURE;
		}
	}

	delete(&root,17);

	return 0;
}

以上代碼寫的比較隨意,基本上保證了代碼邏輯上沒有什麼問題,也闡述了紅黑樹的插入和刪除操作,但是存在一些編碼風格和小的BUG的風險,等到真正使用的時候再去仔細的修訂吧。但是,若您看出了比較明顯的BUG,還請指出,以免錯誤誤導了更多人,謝謝!

 

好了,到此爲止關於二叉搜索樹的故事就告一段落了,一路走了,我們從BSTAVL,再到RB-Tree,操作越來越複雜,但是其性能也是越來越好,關於其他的一些變種,以後就不介紹了,掌握了這些,將來再去學習那些變種樹也會得心應手了。



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