紅黑樹演算法和應用(更高階的二叉查詢樹)
阿新 • • 發佈:2018-12-23
紅黑樹(R-B TREE,全稱:Red-Black Tree),本身是一棵二叉查詢樹,在其基礎上附加了兩個要求:
圖 1 紅黑樹
對於一棵具有 n 個結點的紅黑樹,樹的高度至多為:2lg(n+1)。
旋轉操作 分為左旋和右旋,同二叉排序樹轉平衡二叉樹的旋轉原理完全相同。例如圖 2 表示的是對一棵二叉查詢樹中區域性子樹進行左旋和右旋操作:
圖 2 旋轉操作
左旋:如圖 2 所示,左旋時 y 結點變為該部分子樹的根結點,同時 x 結點(連同其左子樹 a)移動至 y 結點的左孩子。若 y 結點有左孩子 b,由於 x 結點需佔用其位置,所以調整至 x 結點的右孩子處。
左旋操作的具體實現函式:
右旋的具體程式碼實現:
插入結點的第 1 步和第 2 步都非常簡單,關鍵在於最後一步對樹的調整!在紅黑樹中插入結點時,根據插入位置的不同可分為以下 3 種情況:
分析:此種情況下,由於父結點和當前結點顏色都是紅色,所以為了不產生衝突,將父結點的顏色改為黑色。但是雖避免了破壞第 4 條,但是卻導致該條路徑上的黑高度增加了 1 ,破壞了第 5 條性質。但是在將祖父結點顏色改為紅色、叔叔結點顏色改為黑色後,該部分子樹沒有破壞第 5 條性質。但是由於將祖父結點的顏色改變,還需判斷是否破壞了上層樹的結構,所以需要將祖父結點看做當前結點,繼續判斷。
提示:在進行以父結點為當前結點的左旋操作後,此種情況就轉變成了第 3 種情況,處理過程跟第 3 種情況同步進行。
分析:在此種情況下,由於當前結點 F 和父結點 S 顏色都為紅色,違背了紅黑樹的性質 4,此時可以將 S 顏色改為黑色,有違反了性質 5,因為所有通過 S 的路徑其黑高度都增加了 1 ,所以需要將其祖父結點顏色設為紅色後緊接一個右旋,這樣這部分子樹有成為了紅黑樹。(上圖中的有圖雖看似不是紅黑樹,但是隻是整棵樹的一部分,以 S 為根結點的子樹一定是一棵紅黑樹)
紅黑樹中插入結點的具體實現程式碼:
在二叉查詢樹刪除結點時,分為 3 種情況:
以上三種情況最終都需要刪除某個結點,此時需要判斷刪除該結點是否會破壞紅黑樹的性質。判斷的依據是:
紅黑樹刪除結點具體實現程式碼為:
中序遍歷 :1B 2R 3B 4R 5B
前序遍歷 :4B 1B 2R 5B
中序遍歷 :1B 2R 4B 5B
同平衡二叉樹相比較,紅黑樹沒有像平衡二叉樹對平衡性要求的那麼苛刻,雖然兩者的時間複雜度相同,但是紅黑樹在實際測算中的速度要更勝一籌!
- 樹中的每個結點增加了一個用於儲存顏色的標誌域;
- 樹中沒有一條路徑比其他任何路徑長出兩倍,整棵樹要接近於“平衡”的狀態。
紅黑樹對於結點的顏色設定不是任意的,需滿足以下性質的二叉查詢樹才是紅黑樹:這裡所指的路徑,指的是從任何一個結點開始,一直到其子孫的葉子結點的長度;接近於平衡:紅黑樹並不是平衡二叉樹,只是由於對各路徑的長度之差有限制,所以近似於平衡的狀態。
- 樹中的每個結點顏色不是紅的,就是黑的;
- 根結點的顏色是黑的;
- 所有為 nil 的葉子結點的顏色是黑的;(注意:葉子結點說的只是為空(nil 或 NULL)的葉子結點!)
- 如果此結點是紅的,那麼它的兩個孩子結點全部都是黑的;
- 對於每個結點,從該結點到到該結點的所有子孫結點的所有路徑上包含有相同數目的黑結點;
圖 1 紅黑樹
紅黑樹中每個結點都有各自的黑高度,整棵樹也有自己的黑高度,即為根結點的黑高度,例如圖 1 中的紅黑樹的黑高度為 3。注意:圖中每個結點附帶一個整形數值,表示的是此結點的黑高度(從該結點到其子孫結點中包含的黑結點數,用 bh(x) 表示(x 表示此結點)),nil 的黑高度為 0,顏色為黑色(在程式設計時為節省空間,所有的 nil 共用一個儲存空間)。在計算黑高度時,也看做是一個黑結點。
對於一棵具有 n 個結點的紅黑樹,樹的高度至多為:2lg(n+1)。
紅黑樹本身作為一棵二叉查詢樹,所以其任務就是用於動態表中資料的插入和刪除的操作。在進行該操作時,避免不了會破壞紅黑樹的結構,此時就需要進行適當的調整,使其重新成為一棵紅黑樹,可以從兩個方面著手對樹進行調整:由此可推出紅黑樹進行查詢操作時的時間複雜度為
O(lgn)
,因為對於高度為 h 的二叉查詢樹的執行時間為O(h)
,而包含有 n 個結點的紅黑樹本身就是最高為 lgn(簡化之後)的查詢樹(h=lgn),所以紅黑樹的時間複雜度為O(lgn)
。
- 調整樹中某些結點的指標結構;
- 調整樹中某些結點的顏色;
紅黑樹的旋轉
當使用紅黑樹進行插入或者刪除結點的操作時,可能會破壞紅黑樹的 5 條性質,從而變成了一棵普通樹,此時就可以通過對樹中的某些子樹進行旋轉,從而使整棵樹重新變為一棵紅黑樹。旋轉操作
圖 2 旋轉操作
左旋:如圖 2 所示,左旋時 y 結點變為該部分子樹的根結點,同時 x 結點(連同其左子樹 a)移動至 y 結點的左孩子。若 y 結點有左孩子 b,由於 x 結點需佔用其位置,所以調整至 x 結點的右孩子處。
左旋操作的具體實現函式:
//T表示為樹根,x 表示需要進行左旋的子樹的根結點 void rbTree_left_rotate( RBT_Root* T, RB_TREE* x){ RB_TREE* y = x->right;//找到根結點的右子樹 x->right = y->left;//將右子樹的左孩子移動至結點 x 的右孩子處 if(x->right != T->nil){//如果 x 的右子樹不是nil,需重新連線 右子樹的雙親結點為 x x->right->p = x; } y->p = x->p;//設定 y 的雙親結點為 x 的雙親結點 //重新設定 y 的雙親結點同 y 的連線,分為 2 種情況:1、原 x 結點本身就是整棵樹的數根結點,此時只需要將 T 指標指向 y;2、根據 y 中關鍵字同其父結點關鍵字的值的大小,判斷 y 是父結點的左孩子還是右孩子 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 連線給 y 結點的左孩子處 x->p = y;//設定 x 的雙親結點為 y。 }右旋:如圖 2 所示,同左旋是同樣的道理,x 結點變為根結點,同時 y 結點連同其右子樹 c 作為 x 結點的右子樹,原 x 結點的右子樹 b 變為 y 結點的左子樹。
右旋的具體程式碼實現:
void rbTree_right_rotate( RBT_Root* T, RB_TREE* x){ RB_TREE * 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; }
紅黑樹中插入新結點
當建立一個紅黑樹或者向已有紅黑樹中插入新的資料時,只需要按部就班地執行以下 3 步:- 由於紅黑樹本身是一棵二叉查詢樹,所以在插入新的結點時,完全按照二叉查詢樹插入結點的方法,找到新結點插入的位置;
- 將新插入的結點結點初始化,顏色設定為紅色後插入到指定位置;(將新結點初始化為紅色插入後,不會破壞紅黑樹第 5 條的性質)
- 由於插入新的結點,可能會破壞紅黑樹第 4 條的性質(若其父結點顏色為紅色,就破壞了紅黑樹的性質),此時需要調整二叉查詢樹,想辦法通過旋轉以及修改樹中結點的顏色,使其重新成為紅黑樹!
插入結點的第 1 步和第 2 步都非常簡單,關鍵在於最後一步對樹的調整!在紅黑樹中插入結點時,根據插入位置的不同可分為以下 3 種情況:
- 插入位置為整棵樹的樹根。處理辦法:只需要將插入結點的顏色改為黑色即可。
- 插入位置的雙親結點的顏色為黑色。處理方法:此種情況不需要做任何工作,新插入的顏色為紅色的結點不會破壞紅黑樹的性質。
- 插入位置的雙親結點的顏色為紅色。處理方法:由於插入結點顏色為紅色,其雙親結點也為紅色,破壞了紅黑樹第 4 條性質,此時需要結合其祖父結點和祖父結點的另一個孩子結點(父結點的兄弟結點,此處稱為“叔叔結點”)的狀態,分為 3 種情況討論:
- 當前結點的父節點是紅色,且“叔叔結點”也是紅色:破壞了紅黑樹的第 4 條性質,解決方案為:將父結點顏色改為黑色;將叔叔結點顏色改為黑色;將祖父結點顏色改為紅色;下一步將祖父結點認做當前結點,繼續判斷,處理結果如下圖所示:
分析:此種情況下,由於父結點和當前結點顏色都是紅色,所以為了不產生衝突,將父結點的顏色改為黑色。但是雖避免了破壞第 4 條,但是卻導致該條路徑上的黑高度增加了 1 ,破壞了第 5 條性質。但是在將祖父結點顏色改為紅色、叔叔結點顏色改為黑色後,該部分子樹沒有破壞第 5 條性質。但是由於將祖父結點的顏色改變,還需判斷是否破壞了上層樹的結構,所以需要將祖父結點看做當前結點,繼續判斷。
- 當前結點的父結點顏色為紅色,叔叔結點顏色為黑色,且當前結點是父結點的右孩子。解決方案:將父結點作為當前結點做左旋操作。
提示:在進行以父結點為當前結點的左旋操作後,此種情況就轉變成了第 3 種情況,處理過程跟第 3 種情況同步進行。
- 當前結點的父結點顏色為紅色,叔叔結點顏色為黑色,且當前結點是父結點的左孩子。解決方案:將父結點顏色改為黑色,祖父結點顏色改為紅色,從祖父結點處進行右旋處理。如下圖所示:
分析:在此種情況下,由於當前結點 F 和父結點 S 顏色都為紅色,違背了紅黑樹的性質 4,此時可以將 S 顏色改為黑色,有違反了性質 5,因為所有通過 S 的路徑其黑高度都增加了 1 ,所以需要將其祖父結點顏色設為紅色後緊接一個右旋,這樣這部分子樹有成為了紅黑樹。(上圖中的有圖雖看似不是紅黑樹,但是隻是整棵樹的一部分,以 S 為根結點的子樹一定是一棵紅黑樹)
紅黑樹中插入結點的具體實現程式碼:
void RB_Insert_Fixup(RBT_Root* T, RB_TREE* x){ //首先判斷其父結點顏色為紅色時才需要調整;為黑色時直接插入即可,不需要調整 while (x->p->color == RED) { //由於還涉及到其叔叔結點,所以此處需分開討論,確定父結點是祖父結點的左孩子還是右孩子 if (x->p == x->p->p->left) { RB_TREE * y = x->p->p->right;//找到其叔叔結點 //如果叔叔結點顏色為紅色,此為第 1 種情況,處理方法為:父結點顏色改為黑色;叔叔結點顏色改為黑色;祖父結點顏色改為紅色,將祖父結點賦值為當前結點,繼續判斷; if (y->color == RED) { x->p->color = BLACK; y->color = BLACK; x->p->p->color = RED; x = x->p->p; }else{ //反之,如果叔叔結點顏色為黑色,此處需分為兩種情況:1、當前結點時父結點的右孩子;2、當前結點是父結點的左孩子 if (x == x->p->right) { //第 2 種情況:當前結點時父結點的右孩子。解決方案:將父結點作為當前結點做左旋操作。 x = x->p; rbTree_left_rotate(T, x); }else{ //第 3 種情況:當前結點是父結點的左孩子。解決方案:將父結點顏色改為黑色,祖父結點顏色改為紅色,從祖父結點處進行右旋處理。 x->p->color = BLACK; x->p->p->color = RED; rbTree_right_rotate(T, x->p->p); } } }else{//如果父結點時祖父結點的右孩子,換湯不換藥,只需將以上程式碼部分中的left改為right即可,道理是一樣的。 RB_TREE * y = x->p->p->left; if (y->color == RED) { x->p->color = BLACK; y->color = BLACK; x->p->p->color = RED; x = x->p->p; }else{ if (x == x->p->left) { x = x->p; rbTree_right_rotate(T, x); }else{ x->p->color = BLACK; x->p->p->color = RED; rbTree_left_rotate(T, x->p->p); } } } } T->root->color = BLACK; } //插入操作分為 3 步:1、將紅黑樹當二叉查詢樹,找到其插入位置;2、初始化插入結點,將新結點的顏色設為紅色;3、通過呼叫調整函式,將二叉查詢樹重新改為紅黑樹 void rbTree_insert(RBT_Root**T, int k){ //1、找到其要插入的位置。解決思路為:從樹的根結點開始,通過不斷的同新結點的值進行比較,最終找到插入位置 RB_TREE * x, *p; x = (*T)->root; p = x; while(x != (*T)->nil){ p = x; if(k<x->key){ x = x->left; }else if(k>x->key){ x = x->right; }else{ printf("\n%d已存在\n",k); return; } } //初始化結點,將新結點的顏色設為紅色 x = (RB_TREE *)malloc(sizeof(RB_TREE)); x->key = k; x->color = RED; x->left = x->right =(*T)->nil; x->p = p; //對新插入的結點,建立與其父結點之間的聯絡 if((*T)->root == (*T)->nil){ (*T)->root = x; }else if(k < p->key){ p->left = x; }else{ p->right = x; } //3、對二叉查詢樹進行調整 RB_Insert_Fixup((*T),x); }
紅黑樹中刪除結點
在紅黑樹中刪除結點,思路更簡單,只需要完成 2 步操作:- 將紅黑樹按照二叉查詢樹刪除結點的方法刪除指定結點;
- 重新調整刪除結點後的樹,使之重新成為紅黑樹;(還是通過旋轉和重新著色的方式進行調整)
在二叉查詢樹刪除結點時,分為 3 種情況:
- 若該刪除結點本身是葉子結點,則可以直接刪除;
- 若只有一個孩子結點(左孩子或者右孩子),則直接讓其孩子結點頂替該刪除結點;
- 若有兩個孩子結點,則找到該結點的右子樹中值最小的葉子結點來頂替該結點,然後刪除這個值最小的葉子結點。
以上三種情況最終都需要刪除某個結點,此時需要判斷刪除該結點是否會破壞紅黑樹的性質。判斷的依據是:
- 如果刪除結點的顏色為紅色,則不會破壞;
- 如果刪除結點的顏色為黑色,則肯定會破壞紅黑樹的第 5 條性質,此時就需要對樹進行調整,調整方案分 4 種情況討論:
- 刪除結點的兄弟結點顏色是紅色,調整措施為:將兄弟結點顏色改為黑色,父親結點改為紅色,以父親結點來進行左旋操作,同時更新刪除結點的兄弟結點(左旋後兄弟結點發生了變化),如下圖所示:
- 刪除結點的兄弟結點及其孩子全部都是黑色的,調整措施為:將刪除結點的兄弟結點設為紅色,同時設定刪除結點的父結點標記為新的結點,繼續判斷;
- 刪除結點的兄弟結點是黑色,其左孩子是紅色,右孩子是黑色。調整措施為:將兄弟結點設為紅色,兄弟結點的左孩子結點設為黑色,以兄弟結點為準進行右旋操作,最終更新刪除結點的兄弟結點;
- 刪除結點的兄弟結點是黑色,其右孩子是紅色(左孩子不管是什麼顏色),調整措施為:將刪除結點的父結點的顏色賦值給其兄弟結點,然後再設定父結點顏色為黑色,兄弟結點的右孩子結點為黑色,根據其父結點做左旋操作,最後設定替換刪除結點的結點為根結點;
紅黑樹刪除結點具體實現程式碼為:
void rbTree_transplant(RBT_Root* T, RB_TREE* u, RB_TREE* 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; } void RB_Delete_Fixup(RBT_Root**T,RB_TREE*x){ while(x != (*T)->root && x->color == BLACK){ if(x == x->p->left){ RB_TREE* w = x->p->right; //第 1 種情況:兄弟結點是紅色的 if(RED == w->color){ w->color = BLACK; w->p->color = RED; rbTree_left_rotate((*T),x->p); w = x->p->right; } //第2種情況:兄弟是黑色的,並且兄弟的兩個兒子都是黑色的。 if(w->left->color == BLACK && w->right->color == BLACK){ w->color = RED; x = x->p; } //第3種情況 if(w->left->color == RED && w->right->color == BLACK){ w->left->color = BLACK; w->color = RED; rbTree_right_rotate((*T),w); w = x->p->right; } //第4種情況 if (w->right->color == RED) { w->color = x->p->color; x->p->color = BLACK; w->right->color = BLACK; rbTree_left_rotate((*T),x->p); x = (*T)->root; } }else{ RB_TREE* w = x->p->left; //第 1 種情況 if(w->color == RED){ w->color = BLACK; x->p->color = RED; rbTree_right_rotate((*T),x->p); w = x->p->left; } //第 2 種情況 if(w->left->color == BLACK && w->right->color == BLACK){ w->color = RED; x = x->p; } //第 3 種情況 if(w->left->color == BLACK && w->right->color == RED){ w->color = RED; w->right->color = BLACK; w = x->p->left; } //第 4 種情況 if (w->right->color == BLACK){ w->color=w->p->color; x->p->color = BLACK; w->left->color = BLACK; rbTree_right_rotate((*T),x->p); x = (*T)->root; } } } x->color = BLACK;//最終將根結點的顏色設為黑色 } void rbTree_delete(RBT_Root* *T, int k){ if(NULL == (*T)->root){ return ; } //找到要被刪除的結點 RB_TREE * toDelete = (*T)->root; RB_TREE * x = NULL; //找到值為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; } //如果兩個孩子,就找到右子樹中最小的結點,將之代替,然後直接刪除該結點即可 if(toDelete->left != (*T)->nil && toDelete->right != (*T)->nil){ RB_TREE* alternative = rbt_findMin((*T), toDelete->right); k = toDelete->key = alternative->key;//這裡只對值進行復制,並不複製顏色,以免破壞紅黑樹的性質 toDelete = alternative; } //如果只有一個孩子結點(只有左孩子或只有右孩子),直接用孩子結點頂替該結點位置即可(沒有孩子結點的也走此判斷語句)。 if(toDelete->left == (*T)->nil){ x = toDelete->right; rbTree_transplant((*T),toDelete,toDelete->right); }else if(toDelete->right == (*T)->nil){ x = toDelete->left; rbTree_transplant((*T),toDelete,toDelete->left); } //在刪除該結點之前,需判斷此結點的顏色:如果是紅色,直接刪除,不會破壞紅黑樹;若是黑色,刪除後會破壞紅黑樹的第 5 條性質,需要對樹做調整。 if(toDelete->color == BLACK){ RB_Delete_Fixup(T,x); } //最終可以徹底刪除要刪除的結點,釋放其佔用的空間 free(toDelete); }
本節完整實現程式碼
#include <stdio.h> #include <stdlib.h> typedef enum {RED, BLACK} ColorType; typedef struct RB_TREE{ int key; struct RB_TREE * left; struct RB_TREE * right; struct RB_TREE * p; ColorType color; }RB_TREE; typedef struct RBT_Root{ RB_TREE* root; RB_TREE* nil; }RBT_Root; RBT_Root* rbTree_init(void); void rbTree_insert(RBT_Root* *T, int k); void rbTree_delete(RBT_Root* *T, int k); void rbTree_transplant(RBT_Root* T, RB_TREE* u, RB_TREE* v); void rbTree_left_rotate( RBT_Root* T, RB_TREE* x); void rbTree_right_rotate( RBT_Root* T, RB_TREE* x); void rbTree_inPrint(RBT_Root* T, RB_TREE* t); void rbTree_prePrint(RBT_Root * T, RB_TREE* t); void rbTree_print(RBT_Root* T); RB_TREE* rbt_findMin(RBT_Root * T, RB_TREE* t); RB_TREE* rbt_findMin(RBT_Root * T, RB_TREE* t){ if(t == T->nil){ return T->nil; } while(t->left != T->nil){ t = t->left; } return t; } RBT_Root* rbTree_init(void){ RBT_Root* T; T = (RBT_Root*)malloc(sizeof(RBT_Root)); T->nil = (RB_TREE*)malloc(sizeof(RB_TREE)); T->nil->color = BLACK; T->nil->left = T->nil->right = NULL; T->nil->p = NULL; T->root = T->nil; return T; } void RB_Insert_Fixup(RBT_Root* T, RB_TREE* x){ //首先判斷其父結點顏色為紅色時才需要調整;為黑色時直接插入即可,不需要調整 while (x->p->color == RED) { //由於還涉及到其叔叔結點,所以此處需分開討論,確定父結點是祖父結點的左孩子還是右孩子 if (x->p == x->p->p->left) { RB_TREE * y = x->p->p->right;//找到其叔叔結點 //如果叔叔結點顏色為紅色,此為第 1 種情況,處理方法為:父結點顏色改為黑色;叔叔結點顏色改為黑色;祖父結點顏色改為紅色,將祖父結點賦值為當前結點,繼續判斷; if (y->color == RED) { x->p->color = BLACK; y->color = BLACK; x->p->p->color = RED; x = x->p->p; }else{ //反之,如果叔叔結點顏色為黑色,此處需分為兩種情況:1、當前結點時父結點的右孩子;2、當前結點是父結點的左孩子 if (x == x->p->right) { //第 2 種情況:當前結點時父結點的右孩子。解決方案:將父結點作為當前結點做左旋操作。 x = x->p; rbTree_left_rotate(T, x); }else{ //第 3 種情況:當前結點是父結點的左孩子。解決方案:將父結點顏色改為黑色,祖父結點顏色改為紅色,從祖父結點處進行右旋處理。 x->p->color = BLACK; x->p->p->color = RED; rbTree_right_rotate(T, x->p->p); } } }else{//如果父結點時祖父結點的右孩子,換湯不換藥,只需將以上程式碼部分中的left改為right即可,道理是一樣的。 RB_TREE * y = x->p->p->left; if (y->color == RED) { x->p->color = BLACK; y->color = BLACK; x->p->p->color = RED; x = x->p->p; }else{ if (x == x->p->left) { x = x->p; rbTree_right_rotate(T, x); }else{ x->p->color = BLACK; x->p->p->color = RED; rbTree_left_rotate(T, x->p->p); } } } } T->root->color = BLACK; } //插入操作分為 3 步:1、將紅黑樹當二叉查詢樹,找到其插入位置;2、初始化插入結點,將新結點的顏色設為紅色;3、通過呼叫調整函式,將二叉查詢樹重新改為紅黑樹 void rbTree_insert(RBT_Root**T, int k){ //1、找到其要插入的位置。解決思路為:從樹的根結點開始,通過不斷的同新結點的值進行比較,最終找到插入位置 RB_TREE * x, *p; x = (*T)->root; p = x; while(x != (*T)->nil){ p = x; if(k<x->key){ x = x->left; }else if(k>x->key){ x = x->right; }else{ printf("\n%d已存在\n",k); return; } } //初始化結點,將新結點的顏色設為紅色 x = (RB_TREE *)malloc(sizeof(RB_TREE)); x->key = k; x->color = RED; x->left = x->right =(*T)->nil; x->p = p; //對新插入的結點,建立與其父結點之間的聯絡 if((*T)->root == (*T)->nil){ (*T)->root = x; }else if(k < p->key){ p->left = x; }else{ p->right = x; } //3、對二叉查詢樹進行調整 RB_Insert_Fixup((*T),x); } void rbTree_transplant(RBT_Root* T, RB_TREE* u, RB_TREE* 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; } void RB_Delete_Fixup(RBT_Root**T,RB_TREE*x){ while(x != (*T)->root && x->color == BLACK){ if(x == x->p->left){ RB_TREE* w = x->p->right; //第 1 種情況:兄弟結點是紅色的 if(RED == w->color){ w->color = BLACK; w->p->color = RED; rbTree_left_rotate((*T),x->p); w = x->p->right; } //第2種情況:兄弟是黑色的,並且兄弟的兩個兒子都是黑色的。 if(w->left->color == BLACK && w->right->color == BLACK){ w->color = RED; x = x->p; } //第3種情況 if(w->left->color == RED && w->right->color == BLACK){ w->left->color = BLACK; w->color = RED; rbTree_right_rotate((*T),w); w = x->p->right; } //第4種情況 if (w->right->color == RED) { w->color = x->p->color; x->p->color = BLACK; w->right->color = BLACK; rbTree_left_rotate((*T),x->p); x = (*T)->root; } }else{ RB_TREE* w = x->p->left; //第 1 種情況 if(w->color == RED){ w->color = BLACK; x->p->color = RED; rbTree_right_rotate((*T),x->p); w = x->p->left; } //第 2 種情況 if(w->left->color == BLACK && w->right->color == BLACK){ w->color = RED; x = x->p; } //第 3 種情況 if(w->left->color == BLACK && w->right->color == RED){ w->color = RED; w->right->color = BLACK; w = x->p->left; } //第 4 種情況 if (w->right->color == BLACK){ w->color=w->p->color; x->p->color = BLACK; w->left->color = BLACK; rbTree_right_rotate((*T),x->p); x = (*T)->root; } } } x->color = BLACK;//最終將根結點的顏色設為黑色 } void rbTree_delete(RBT_Root* *T, int k){ if(NULL == (*T)->root){ return ; } //找到要被刪除的結點 RB_TREE * toDelete = (*T)->root; RB_TREE * x = NULL; //找到值為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; } //如果兩個孩子,就找到右子樹中最小的結點,將之代替,然後直接刪除該結點即可 if(toDelete->left != (*T)->nil && toDelete->right != (*T)->nil){ RB_TREE* alternative = rbt_findMin((*T), toDelete->right); k = toDelete->key = alternative->key;//這裡只對值進行復制,並不複製顏色,以免破壞紅黑樹的性質 toDelete = alternative; } //如果只有一個孩子結點(只有左孩子或只有右孩子),直接用孩子結點頂替該結點位置即可(沒有孩子結點的也走此判斷語句)。 if(toDelete->left == (*T)->nil){ x = toDelete->right; rbTree_transplant((*T),toDelete,toDelete->right); }else if(toDelete->right == (*T)->nil){ x = toDelete->left; rbTree_transplant((*T),toDelete,toDelete->left); } //在刪除該結點之前,需判斷此結點的顏色:如果是紅色,直接刪除,不會破壞紅黑樹;若是黑色,刪除後會破壞紅黑樹的第 5 條性質,需要對樹做調整。 if(toDelete->color == BLACK){ RB_Delete_Fixup(T,x); } //最終可以徹底刪除要刪除的結點,釋放其佔用的空間 free(toDelete); } //T表示為樹根,x 表示需要進行左旋的子樹的根結點 void rbTree_left_rotate( RBT_Root* T, RB_TREE* x){ RB_TREE* y = x->right;//找到根結點的右子樹 x->right = y->left;//將右子樹的左孩子移動至結點 x 的右孩子處 if(x->right != T->nil){//如果 x 的右子樹不是nil,需重新連線 右子樹的雙親結點為 x x->right->p = x; } y->p = x->p;//設定 y 的雙親結點為 x 的雙親結點 //重新設定 y 的雙親結點同 y 的連線,分為 2 種情況:1、原 x 結點本身就是整棵樹的數根結點,此時只需要將 T 指標指向 y;2、根據 y 中關鍵字同其父結點關鍵字的值的大小,判斷 y 是父結點的左孩子還是右孩子 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 連線給 y 結點的左孩子處 x->p = y;//設定 x 的雙親結點為 y。 } void rbTree_right_rotate( RBT_Root* T, RB_TREE* x){ RB_TREE * 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 rbTree_prePrint(RBT_Root* T, RB_TREE* t){ if(T->nil == t){ return; } if(t->color == RED){ printf("%dR ",t->key); }else{ printf("%dB ",t->key); } rbTree_prePrint(T,t->left); rbTree_prePrint(T,t->right); } void rbTree_inPrint(RBT_Root* T, RB_TREE* t){ if(T->nil == t){ return ; } rbTree_inPrint(T,t->left); if(t->color == RED){ printf("%dR ",t->key); }else{ printf("%dB ",t->key); } rbTree_inPrint(T,t->right); } //輸出紅黑樹的前序遍歷和中序遍歷的結果 void rbTree_print(RBT_Root* T){ printf("前序遍歷 :"); rbTree_prePrint(T,T->root); printf("\n"); printf("中序遍歷 :"); rbTree_inPrint(T,T->root); printf("\n"); } int main(){ RBT_Root* T = rbTree_init(); rbTree_insert(&T,3); rbTree_insert(&T,5); rbTree_insert(&T,1); rbTree_insert(&T,2); rbTree_insert(&T,4); rbTree_print(T); printf("\n"); rbTree_delete(&T,3); rbTree_print(T); return 0; }執行結果: 前序遍歷 :3B 1B 2R 5B 4R
中序遍歷 :1B 2R 3B 4R 5B
前序遍歷 :4B 1B 2R 5B
中序遍歷 :1B 2R 4B 5B
總結
本節介紹的紅黑樹,雖隸屬於二叉查詢樹,但是二叉查詢樹的時間複雜度會受到其樹深度的影響,而紅黑樹可以保證在最壞情況下的時間複雜度仍為O(lgn)
。當資料量多到一定程度時,使用紅黑樹比二叉查詢樹的效率要高。同平衡二叉樹相比較,紅黑樹沒有像平衡二叉樹對平衡性要求的那麼苛刻,雖然兩者的時間複雜度相同,但是紅黑樹在實際測算中的速度要更勝一籌!
提示:平衡二叉樹的時間複雜度是
O(logn)
,紅黑樹的時間複雜度為O(lgn)
,兩者都表示的都是時間複雜度為對數關係(lg 函式為底是 10 的對數,用於表示時間複雜度時可以忽略)。