紅黑樹(附完整C程式碼)
紅黑樹簡介
首先紅黑樹是一棵二叉搜尋樹,它在每個結點上增加了一個儲存位來表示結點的顏色,可以是RED或者BLACK。通過對一條從根節點到NIL葉節點(指空結點或者下面說的哨兵)的簡單路徑上各個結點在顏色進行約束,紅黑樹確保沒有一條路徑會比其他路徑長出2倍,因而是近似平衡的。
用途
紅黑樹和AVL樹一樣都對插入時間、刪除時間和查詢時間提供了最好可能的最壞情況擔保。對於查詢、插入、刪除、最大、最小等動態操作的時間複雜度為O(lgn).常見的用途有以下幾種:
- STL(標準模板庫)中在set map是基於紅黑樹實現的。
- Java中在TreeMap使用的也是紅黑樹。
- epoll在核心中的實現,用紅黑樹管理事件塊。
- linux程序排程Completely Fair Scheduler,用紅黑樹管理程序控制塊
紅黑樹 VS AVL樹
常見的平衡樹有紅黑樹和AVL平衡樹,為什麼STL和linux都使用紅黑樹作為平衡樹的實現?大概有以下幾個原因:
從實現細節上來講,如果插入一個結點引起了樹的不平衡,AVL樹和紅黑樹都最多需要2次旋轉操作,即兩者都是O(1);但是在刪除node引起樹的不平衡時,最壞情況下,AVL需要維護從被刪node到root這條路徑上所有node的平衡性,因此需要旋轉的量級O(logN),而RB-Tree最多隻需3次旋轉,只需要O(1)的複雜度
從兩種平衡樹對平衡的要求來講,AVL的結構相較RB-Tree來說更為平衡,在插入和刪除node更容易引起Tree的unbalance,因此在大量資料需要插入或者刪除時,AVL需要rebalance的頻率會更高。因此,RB-Tree在需要大量插入和刪除node的場景下,效率更高。自然,由於AVL高度平衡,因此AVL的search效率更高。
總體來說,RB-tree的統計效能是高於AVL的。
關於參考書
《演算法導論》和《資料結構與演算法分析》是大家常用的兩本演算法書,針對紅黑樹這一章,這兩本書上也都有,但是二者從資料結構到使用的方法上都不一樣,這裡我推薦使用《演算法導論》。有以下幾個原因:
- 《資料結構與演算法分析》中使用的資料結構沒有儲存父親結點,所以在呼叫旋轉函式時需要用函式返回值來保持上下結點的連線,這在avl樹中顯得很簡潔,因為在avl中的情況比較簡單,但是在紅黑樹中涉及到了祖祖父、祖父、父親、兒子、四代結點,按照這本書上的方法得儲存GGP、GP、P、X四個全域性變數的值,在更新它們的指向時非常容易搞混。
- 《資料結構與演算法分析》沒有實現刪除操作,只是描述了實現的方法,按照這上面的方法我也做了實現,最後發現程式碼非常長,至少150+,不如《演算法導論》中的簡潔。
- 另外,《演算法導論》中描述的方法比較完整,思路嚴謹。
紅黑樹詳解
紅黑樹性質
紅黑樹是每個結點都帶有顏色屬性的二叉查詢樹,顏色為紅色或黑色。在二叉查詢樹強制一般要求以外,對於任何有效的紅黑樹我們增加了如下的額外要求:
- 列表項結點是紅色或黑色。
- 根是黑色。
- 所有葉子都是黑色(葉子是NIL結點)。
- 每個紅色結點必須有兩個黑色的子結點。(從每個葉子到根的所有路徑上不能有兩個連續的紅色結點。)
- 從任一結點到其每個葉子的所有簡單路徑都包含相同數目的黑色結點。
為了便於處理紅黑樹中的邊界情況,使用一個哨兵來代表所有的NIL結點,也就是說所有指向NIL的指標都指向哨兵T.nil。
紅黑樹資料結構
typedef enum ColorType {RED, BLACK} ColorType;
typedef struct rbt_t{
int key;
rbt_t * left;
rbt_t * right;
rbt_t * p;
ColorType color;
}rbt_t;
typedef struct rbt_root_t{
rbt_t* root;
rbt_t* nil;
}rbt_root_t;
/*
*@brief rbt_init 初始化
*/
rbt_root_t* rbt_init(void){
rbt_root_t* T;
T = (rbt_root_t*)malloc(sizeof(rbt_root_t));
assert( NULL != T);
//用一個哨兵代表NIL。
T->nil = (rbt_t*)malloc(sizeof(rbt_t));
assert(NULL != T->nil);
T->nil->color = BLACK;
T->nil->left = T->nil->right = NULL;
T->nil->p = NULL;
T->root = T->nil;
return T;
}
紅黑樹旋轉
搜尋樹操作inert和delete在含有n個關鍵字的紅黑樹上,執行花費時間為
指標結構的修改是通過旋轉來完成的,這是一種能保持二叉搜尋樹性質的區域性操作。一種有兩種旋轉操作,如下圖所示,都在O(1)時間內完成。
這裡要求x,y都不是T.nil。
c程式碼:
/*
*@brief rbt_left_rotate
*@param[in] T 樹根
*@param[in] x 要進行旋轉的節點
*/
void rbt_left_rotate( rbt_root_t* T, rbt_t* x){
rbt_t* y = x->right;
x->right = y->left;
if(x->right != T->nil)//更新某結點的父親時,要確定此結點不是T.nil
x->right->p = x;
y->p = x->p;
if(x->p == T->nil){//如果x以前是樹根,那麼現在樹根易主了
T->root = y;
}else if(y->key < y->p->key)
y->p->left = y;
else
y->p->right = y;
y->left = x;
x->p = y;
}
/*
*@brief rbt_right_rotate
*@param[in] 樹根
*@param[in] 要進行旋轉的節點
*/
void rbt_right_rotate( rbt_root_t* T, rbt_t* x){
rbt_t * y = x->left;
x->left = y->right;
if(T->nil != x->left)
x->left->p = x;
y->p = x->p;
if(y->p == T->nil)
T->root = y;
else if(y->key < y->p->key)
y->p->left= y;
else
y->p->right = y;
y->right = x;
x->p = y;
}
紅黑樹插入
在紅黑樹中插入一個元素,跟在二叉搜尋樹中插入一個元素一樣,只是插入一個元素之後,有可能使得這個樹不再平衡,所以要再處理一下,使之重新回到平衡狀態。
圖中N為新插入的結點,U為它的叔叔。
插入操作的關鍵在於以下幾點:
- 新插入的節點一定是紅色的。(如果是黑色的,會破壞條件5)
- 如果新插入的節點的父親是黑色的,則沒有破壞任何性質,那麼插入完成。
- 如果插入節點的父節點是紅色, 破壞了性質4. 故插入演算法就是通過重新著色或旋轉, 來維持性質
插入一個紅色節點要處理這麼幾種情況:
此時要記住一件事事情,插入時總是要考慮它的叔叔,刪除時總要考慮它的兄弟。而且插入時維護的主要是顏色(性質4),而刪除時維護的主要是黑色結點數量(性質5)
情況1:
N為紅,P為紅(GP一定為黑),U為紅。
下面會說明我們可以通過一種特殊的處理把這種情況避免掉。
那為什麼要避免這種情況呢?因為這種情況一般是通過顏色翻轉來處理的,也就是把P U換成黑色,把GP抱成紅色,但是GP的父親如果是紅色的話又會違反紅黑樹的性質。
情況2:
N,P都為紅(GP一定為黑),U為黑
根據境像,情況2可細分為4種情況,如下:
但是這四種具體情況的處理手法是一樣的,都是通過顏色翻轉與旋轉來處理的。下面我們通過情況2.1和2.2來說明一下處理方法:
情況2.2通過呼叫left_rotate(T,p)變成情況2.1;
情況2.1通過交換GP與P的顏色,然後呼叫right_rotate(T,GP),此時不再違反任何性質。
情況2.3和2.4分別是2.1和2.2的境像。
如何避免情況1
令X = T.root,在向下遍歷的過程中,我們如果遇到X.right.color == x.left.color == RED時我們將x與它孩子的顏色翻轉,即把x塗成紅色,把x.right和x.left塗成黑色。
如果x的父親為黑色,沒有違反性質;如果x的父親為紅色,那麼可以把x當成新插入的紅色結點N,那麼只需要處理情況2即可。
至此,插入完成,具體實現可以看完整程式碼部分,程式碼也有必要的註釋。
紅黑樹刪除
還是上面同樣那句話,插入時總是要考慮它的叔叔,刪除時總要考慮它的兄弟。而且插入時維護的主要是顏色(性質4),而刪除時維護的主要是黑色結點數量(性質5)。
寫刪除的程式碼花費了我大概一天多的時間,因為我總是試圖找出一種比《資料結構與演算法分析》上更清晰,比《演算法導論》中更簡單的方法,但是失敗了(⊙▽⊙).
實際上刪除操作也沒有那麼難,如果要刪除 z 結點,那麼就讓 z 的後繼來代替 z 的位置即可。 如果z是紅色的,那麼操作便完成了,刪除一個紅色結點沒有違反任何性質。但如果z是黑色的,那麼我們刪除一個黑色結點,便違反了性質5,造成黑色結點數量的左右不平衡。只要分析出刪除一個黑色結點會遇到哪些情況即可。
首先找到要刪除的結點,我們定義它為 z 。
如果 z 的兩個孩子都不是T.nil,那麼我們在 z 的右子樹中找出最小的結點 m,把 m 結點的值賦給 z (而不是把m移植到z的位置,也就不用考慮顏色問題,這一點是比《演算法導論》中要簡單的),那麼我們要刪除的結點就成為 m 了。m 肯定沒有左孩子。令 z 重新指向 m .
找到要刪除的結點 z 之後,我們用 z 的孩子(記作 x )來取代 z的位置(即使z.right == T.nil) 。rbt_transplant(T,z,z.right);
此時用到下面一段程式碼,實現用v代替u
void rbt_transplant(rbt_root_t* T, rbt_t* u, rbt_t* v){
if(u->p == T->nil)
T->root = v;
else if(u == u->p->left)
u->p->left =v;
else
u->p->right = v;
v->p = u->p;//即使v是T.nil也可以執行這一行
}
到目前為止,如果要被刪除的 z 結點是紅色的,那麼程式就結束了。但是如果 z 是黑色的,所以刪除z之後z這邊少了一個黑色結點,會違反性質5,此時分為4種情況(x 是左孩子 和 x 是右孩子分別有4種情況,現在只討論x是左孩子的情況):
情況1
x的兄弟w是紅色的,那麼它們的父親、w的孩子都是黑色的。
這種情況下只能做一種無損的操作,通過交換顏色再旋轉,對樹的性質不會產生影響,所以從根到x結點的路徑上少的一個黑色結點也不會補上。
交換p與w的顏色,再對p進行左旋操之後,x的新兄弟就為黑色,情況變成了2 3 4中的一種.
圖中x為白色,表示我們不關心x的顏色。
情況2
x的兄弟w是黑色,而且w的兩個孩子都是黑色。
此時可以細分為2種情況,但無論哪種情況,我們要進行的操作都是一樣的,都是將w塗成紅色,將p塗成黑色。
如果是情況2.1(有可能由情況1發展過來的),由於上述操作為x那邊補上了一個黑色(從根到x在路徑上多了一個黑色結點),此時紅黑樹性質5得到滿足,程式結束。
如果是情況2.2, 經過上述操作後,P的右子樹也少了一個黑色結點,令P作為新的X繼續迴圈。
情況3
W是黑色有,w在左孩子是紅色的,W的右孩子是黑色的。
通過交換L與W的顏色,再對W進行右旋操作。這種操作也不會對紅黑樹性質產生影響,此時進入情況4,我們會看到通過情況4中的操作最終使紅黑樹性質得到滿足,結束程式。
圖中最後邊的R結點沒有畫出來,因為我們不關心它了
情況4
w是黑色的,w的右孩子是紅色的。
把w塗成p的顏色,把P塗成黑色,R塗成黑色,左旋P。此時從根到x在路徑上多了一個黑色結點,程式結束。
具體實現程式碼見下面。
完整程式碼(C)
#include<stdafx.h>
#include<malloc.h>
#include <assert.h>
//紅黑樹
typedef enum ColorType {RED, BLACK} ColorType;
typedef struct rbt_t{
int key;
rbt_t * left;
rbt_t * right;
rbt_t * p;
ColorType color;
}rbt_t;
typedef struct rbt_root_t{
rbt_t* root;
rbt_t* nil;
}rbt_root_t;
//函式宣告
rbt_root_t* rbt_init(void);
static void rbt_handleReorient(rbt_root_t* T, rbt_t* x, int k);
rbt_root_t* rbt_insert(rbt_root_t* &T, int k);
rbt_root_t* rbt_delete(rbt_root_t* &T, int k);
void rbt_transplant(rbt_root_t* T, rbt_t* u, rbt_t* v);
static void rbt_left_rotate( rbt_root_t* T, rbt_t* x);
static void rbt_right_rotate( rbt_root_t* T, rbt_t* x);
void rbt_inPrint(const rbt_root_t* T, rbt_t* t);
void rbt_prePrint(const rbt_t * T, rbt_t* t);
void rbt_print(const rbt_root_t* T);
static rbt_t* rbt_findMin(rbt_root_t * T, rbt_t* t);
static rbt_t* rbt_findMax(rbt_root_t * T, rbt_t* t);
static rbt_t* rbt_findMin(rbt_root_t * T, rbt_t* t){
if(t == T->nil) return T->nil;
while(t->left != T->nil)
t = t->left;
return t;
}
static rbt_t* rbt_findMax(rbt_root_t * T, rbt_t* t){
if(t == T->nil) return T->nil;
while(t->right != T->nil)
t = t->right;
return t;
}
/*
*@brief rbt_init 初始化
*/
rbt_root_t* rbt_init(void){
rbt_root_t* T;
T = (rbt_root_t*)malloc(sizeof(rbt_root_t));
assert( NULL != T);
T->nil = (rbt_t*)malloc(sizeof(rbt_t));
assert(NULL != T->nil);
T->nil->color = BLACK;
T->nil->left = T->nil->right = NULL;
T->nil->p = NULL;
T->root = T->nil;
return T;
}
/*
*@brief rbt_handleReorient 內部函式 由rbt_insert呼叫
* 在兩種情況下呼叫這個函式:
* 1 x有連個紅色兒子
* 2 x為新插入的結點
*
*/
void rbt_handleReorient(rbt_root_t* T, rbt_t* x, int k){
//在第一種情況下,進行顏色翻轉; 在第二種情況下,相當於對新插入的x點初始化
x->color = RED;
x->left->color = x->right->color = BLACK;
//如果x.p為紅色,那麼x.p一定不是根,x.p.p一定不是T.nil,而且為黑色
if( RED == x->p->color){
x->p->p->color = RED;//此時x, p, x.p.p都為紅
if(x->p->key < x->p->p->key){
if(k > x->p->key){
x->color = BLACK;//小心地處理顏色
rbt_left_rotate(T,x->p);
rbt_right_rotate(T,x->p);
}else{
x->p->color = BLACK;//小心地處理顏色
rbt_right_rotate(T,x->p->p);
}
}else{
if(k < x->p->key){
x->color = BLACK;
rbt_right_rotate(T,x->p);
rbt_left_rotate(T,x->p);
}else{
x->p->color = BLACK;
rbt_left_rotate(T,x->p->p);
}
}
}
T->root->color = BLACK;//無條件令根為黑色
}
/*
*@brief brt_insert 插入
*1 新插入的結點一定是紅色的,如果是黑色的,會破壞條件4(每個結點到null葉結點的每條路徑有同樣數目的黑色結點)
*2 如果新插入的結點的父親是黑色的,那麼插入完成。 如果父親是紅色的,那麼做一個旋轉即可。(前提是叔叔是黑色的)
*3 我們這個插入要保證其叔叔是黑色的。也就是在x下沉過程中,不允許存在兩個紅色結點肩並肩。
*/
rbt_root_t* rbt_insert(rbt_root_t* &T, int k){
rbt_t * x, *p;
x = T->root;
p = x;
//令x下沉到葉子上,而且保證一路上不會有同時為紅色的兄弟
while( x != T->nil){
//
//保證沒有一對兄弟同時為紅色, 為什麼要這麼做?
if(x != T->nil)
if(x->left->color == RED && x->right->color == RED)
rbt_handleReorient(T,x,k);
p = x;
if(k<x->key)
x = x->left;
else if(k>x->key)
x = x->right;
else{
printf("\n%d已存在\n",k);
return T;
}
}
//為x分配空間,並對其進行初始化
x = (rbt_t *)malloc(sizeof(rbt_t));
assert(NULL != x);
x->key = k;
x->color = RED;
x->left = x->right = T->nil;
x->p = p;
//讓x的父親指向x
if(T->root == T->nil)
T->root = x;
else if(k < p->key)
p->left = x;
else
p->right = x;
//因為一路下來,如果x的父親是紅色,那麼x的叔叔肯定不是紅色了,這個時候只需要做一下翻轉即可。
rbt_handleReorient(T,x,k);
return T;
}
void rbt_transplant(rbt_root_t* T, rbt_t* u, rbt_t* v){
if(u->p == T->nil)
T->root = v;
else if(u == u->p->left)
u->p->left =v;
else
u->p->right = v;
v->p = u->p;
}
/*
*@brief rbt_delete 從樹中刪除 k
*
*
*/
rbt_root_t* rbt_delete(rbt_root_t* &T, int k){
assert(T != NULL);
if(NULL == T->root) return T;
//找到要被刪除的葉子結點
rbt_t * toDelete = T->root;
rbt_t * x;
//找到值為k的結點
while(toDelete != T->nil && toDelete->key != k){
if(k<toDelete->key)
toDelete = toDelete->left;
else if(k>toDelete->key)
toDelete = toDelete->right;
}
if(toDelete == T->nil){
printf("\n%d 不存在\n",k);
return T;
}
//如果兩個孩子,就找到右子樹中最小的代替, alternative最多有一個右孩子
if(toDelete->left != T->nil && toDelete->right != T->nil){
rbt_t* alternative = rbt_findMin(T, toDelete->right);
k = toDelete->key = alternative->key;
toDelete = alternative;
}
if(toDelete->left == T->nil){
x = toDelete->right;
rbt_transplant(T,toDelete,toDelete->right);
}else if(toDelete->right == T->nil){
x = toDelete->left;
rbt_transplant(T,toDelete,toDelete->left);
}
if(toDelete->color == BLACK){
//x不是todelete,而是用於代替x的那個
//如果x顏色為紅色的,把x塗成黑色即可, 否則 從根到x處少了一個黑色結點,導致不平衡
while(x != T->root && x->color == BLACK){
if(x == x->p->left){
rbt_t* w = x->p->right;
//情況1 x的兄弟是紅色的,通過
if(RED == w->color){
w->color = BLACK;
w->p->color = RED;
rbt_left_rotate(T,x->p);
w = x->p->right;
}//處理完情況1之後,w.color== BLACK , 情況就變成2 3 4 了
//情況2 x的兄弟是黑色的,並且其兒子都是黑色的。
if(w->left->color == BLACK && w->right->color == BLACK){
if(x->p->color == RED){
x->p->color = BLACK;
w->color = RED;
break;
}else{
w->color = RED;
x = x->p;//x.p左右是平衡的,但是x.p處少了一個黑結點,所以把x.p作為新的x繼續迴圈
continue;
}
}
//情況3 w為黑色的,左孩子為紅色。(走到這一步,說明w左右不同時為黑色。)
if(w->right->color == BLACK){
w->left->color = BLACK;
w->color = RED;
rbt_right_rotate(T,w);
w = x->p->right;
}//處理完之後,變成情況4
//情況4 走到這一步說明w為黑色, w的左孩子為黑色, 右孩子為紅色。
w->color=x->p->color;
x->p->color=BLACK;
w->right->color=BLACK;
rbt_left_rotate(T,x->p);
x = T->root;
}else{
rbt_t* w = x->p->left;
//1
if(w->color == RED){
w->color = BLACK;
x->p->color = RED;
rbt_right_rotate(T,x->p);
w = x->p->left;
}
//2
if(w->left->color==BLACK && w->right->color == BLACK){
if(x->p->color == RED){
x->p->color = BLACK;
w->color = RED;
break;
}else{
x->p->color = BLACK;
w->color = RED;
x = x->p;
continue;
}
}
//3
if(w->left->color == BLACK){
w->color = RED;
w->right->color = BLACK;
w = x->p->left;
}
//4
w->color=w->p->color;
x->p->color = BLACK;
w->left->color = BLACK;
rbt_right_rotate(T,x->p);
x = T->root;
}
}
x->color = BLACK;
}
//放心刪除todelete 吧
free(toDelete);
return T;
}
/*
*@brief rbt_left_rotate
*@param[in] T 樹根
*@param[in] x 要進行旋轉的結點
*/
void rbt_left_rotate( rbt_root_t* T, rbt_t* x){
rbt_t* y = x->right;
x->right = y->left;
if(x->right != T->nil)
x->right->p = x;
y->p = x->p;
if(y->p == T->nil){
T->root = y;
}else if(y->key < y->p->key)
y->p->left = y;
else
y->p->right = y;
y->left = x;
x->p = y;
}
/*
*@brief rbt_right_rotate
*@param[in] 樹根
*@param[in] 要進行旋轉的結點
*/
void rbt_right_rotate( rbt_root_t* T, rbt_t* x){
rbt_t * y = x->left;
x->left = y->right;
if(T->nil != x->left)
x->left->p = x;
y->p = x->p;
if(y->p == T->nil)
T->root = y;
else if(y->key < y->p->key)
y->p->left= y;
else
y->p->right = y;
y->right = x;
x->p = y;
}
void rbt_prePrint(const rbt_root_t* T, rbt_t* t){
if(T->nil == t)return ;
if(t->color == RED)
printf("%3dR",t->key);
else
printf("%3dB",t->key);
rbt_prePrint(T,t->left);
rbt_prePrint(T,t->right);
}
void rbt_inPrint(const rbt_root_t* T, rbt_t* t){
if(T->nil == t)return ;
rbt_inPrint(T,t->left);
if(t->color == RED)
printf("%3dR",t->key);
else
printf("%3dB",t->key);
rbt_inPrint(T,t->right);
}
//列印程式包括前序遍歷和中序遍歷兩個,因為它倆可以唯一確定一棵二叉樹
void rbt_print(const rbt_root_t* T){
assert(T!=NULL);
printf("\n前序遍歷 :");
rbt_prePrint(T,T->root);
printf("\n中序遍歷 :");
rbt_inPrint(T,T->root);
printf("\n");
}
void rbt_test(){
rbt_root_t* T = rbt_init();
/************************************************************************/
/* 1 測試插入
/*
/*
/*輸出 前序遍歷 : 7B 2R 1B 5B 4R 11R 8B 14B 15R
/* 中序遍歷 : 1B 2R 4R 5B 7B 8B 11R 14B 15R
/************************************************************************/
T = rbt_insert(T,11);
T = rbt_insert(T,7);
T = rbt_insert(T,1);
T = rbt_insert(T,2);
T = rbt_insert(T,8);
T = rbt_insert(T,14);
T = rbt_insert(T,15);
T = rbt_insert(T,5);
T = rbt_insert(T,4);
T = rbt_insert(T,4); //重複插入測試
rbt_print(T);
/************************************************************************/
/* 2 測試刪除
/*
/*操作 連續刪除4個元素 rbt_delete(T,8);rbt_delete(T,14);rbt_delete(T,7);rbt_delete(T,11);
/*輸出 前序遍歷 : 2B 1B 5R 4B 15B
/* 中序遍歷 : 1B 2B 4B 5R 15B
/************************************************************************/
rbt_delete(T,8);
rbt_delete(T,14);rbt_delete(T,7);rbt_delete(T,11);
rbt_delete(T,8);//刪除不存在的元素
rbt_print(T);
}
程式碼只做了少量的測試,如果有BUG請不吝指出。