RB-Tree, 紅黑樹(Red Black Tree)
1. 簡介
1.1 定義
首先紅黑樹是一棵二叉搜索樹,節點除了二叉樹基本元素之外,還包括顏色信息,即節點包含key、left、right、p、color以及數據索引6個域。除此之外還需滿足規則123
1.2 定理
-
定理1
紅黑樹中任意兩條路徑 P, Q ,P 和 Q 的路徑長度存在(長度爲內部節點個數)
假設路徑上的黑色節點爲r個,則最短路徑爲 r 個黑色節點,最長路徑爲每個紅色節點後面跟上一個黑色節點,最短路徑長 :r - 1,最長爲 ( r - 1) + (r - 2 )+ 1 = 2(r-1)。
所以上式成立,最長路徑都小於最小路徑的兩倍了 。 -
引理1
根據 2 中的定理知道,紅黑樹高度h ,書節點個數 n
1) h <= 2r;(h 是最長路徑)
2 ) n >= 2r - 1(只有兩條路徑,且都爲最短)
3) h <= 2log2(n + 1)( r <= log2(n + 1)
2. 紅黑樹的操作
紅黑樹的插入和刪除調整本質是
- 插入
將插入引起的多餘紅色向叔分支轉移,如果叔分支紅色已滿,則向爺爺的叔分支轉移。 - 刪除
將刪除導致的黑色減少從兄弟分支借一個紅色節點(是一種顏色上的借)到本側染黑,如果無法借則向叔公借(就是往上一層的兄弟分支借)。
2.1. 旋轉
旋轉是紅黑樹調整中最基本的操縱,包括左旋和右旋,其實旋轉本的質是雙向鏈表的操作(斷鏈,連接鏈)。
- 左旋:
以節點x和其右孩y之間的鏈爲支軸,向左旋轉,y替代x,y的左孩子替代y,x替代y的左孩子。使得y成爲x的雙親,y的左孩子成爲x的右孩子。 - 右旋:
右旋的過程則相反,以x和左孩子y之間的鏈爲軸,是y成爲x的雙親,y的右孩子成爲x的左孩子。
這裏旋轉時節點相對於x和y來說邏輯左右關係沒有變化,操作過程其實本質在執行鏈表的斷鏈和結鏈,一共斷開三條鏈,修復三條鏈。
- 操作流程:
- 先將x從樹中拿掉(直接砍掉放在一邊),直接利用y代替x
- 此時x缺乏了孩子,用y的孩子替代原來y在x中的位置,即補充了x缺的孩子。
- 此時y沒了孩子,x替代y的孩子。
- TODO 補充圖片
以上流程一個蘿蔔一個坑,必須都填好,y替代x時,x的孩子就空缺了,所以y的孩子替代y,y的孩子沒了,那麼x再替代y的孩子,就圓滿了,類似一個swap的操作過程。
void LeftRotate(RbTree *T, RbNode *x) {
RbNode *y = x->right;
// y 替代 x
if (y == NULL) return;
// 處理p和y
if (x->p == NULL)
T.root = y;
else if(x->p->left == x) {
x->p->left = y;
} else {
x->p->right = y;
}
y->p = x->p;
// 到這裏y和p的關係處理完畢,但是x仍然指向該兩個節點,需要調整
// y的孩子替代y
x->right = y->left;
if (x->right != NULL) {
x->right->p = x;
}
// x替代y的孩子
x->p = y;
y->left = x;
}
void RightRotate(RbTree *T, RbNode *x) {
RbNode *y = x->left;
// replace x with y
if (x->p == NULL) {
T.root = y;
}
else if (x->p->left == x) {
x->p->left = y;
} else {
x->p->right = y;
}
y->p = x->p;
// replace y with y's child in x
x->left = y->right;
if (x->left != NULL) {
x->left->p = x;
}
// replace y's child with x
y->right = x;
x->p = y;
}
2.2. 插入
紅黑樹的插入過程同二叉搜索樹,但是插入後需要進行調整,插入新節點要上色,一般如果上黑色肯定違反定義中的規則RB rule 33,如果給上紅色,則有可能違法規則RB rule 22,也有可能不違反。綜合來看插入一個節點優先上紅色,如果出現連續的紅色則進行改色、旋轉的動作進行調整,即紅黑樹的插入除了常規的二叉樹插入操作外,還要進行調整操作。
2.2.1. 插入調整
插入節點z都是紅色,如果引起違反RB rule 22,那麼其父節p點必須是紅色,gp必須爲黑色(grandparent,如果gp不是黑色,那麼上一輪插入p和gp就不是紅黑樹了)。按照p和z所處分支情況可分爲如下幾種情況:
- LL
p在gp左分支,z在p左分支 - LR
p在gp左邊,z在p右邊 - RL
p在gp右邊,z在p右邊 - RR
p在gp右邊,z在p右邊
在以上基礎上再對p的兄弟節點,即z的叔節點進行顏色劃分,可以爲紅色或則黑色,那麼一共是8種情況:
LLr、LRr、RLr、RRr、LLb、LRb、RLb、RRb。調整方法根據以上的情況調整。只需要瞭解調整LLr、LRr、LLb、LRb,其他四種情況和這四種鏡像對稱(即左右對調即可)。
- TODO 以後補充圖片
2.2.2. 調整方法
調整方法本質只有兩種:着色、旋轉,是根據插入節點叔節點的顏色決定的,如果叔節點是紅色則着色,如果是黑色則旋轉。
1. 着色
此種情況叔節點均爲紅,那麼無法向叔分支轉移多餘的紅色,只可以向祖宗求救(轉移)。
- LLr
- LRr
2. 旋轉
此時叔節點均爲黑
- LLb
- LRb
在p上執行一次左旋轉,將LRb轉爲LLb類型,然後再在gp上執行一次右旋轉,將gp改色爲紅色,z改色爲黑色,完成調整。同樣的叔節點第一次時是NULL,遞歸後遇到的則是內部節點。 - 其他
其他四種情況和以上情況鏡像對稱,只需採取鏡像操作即可。
2.2.3. 實現
# define RED 0
# define BLACK 1
void RbTreeInsert(RbTree *T, TreeNode *node) {
if (node == NULL) return;
ket_t key = node->key;
TreeNode *p = NULL;
TreeNode *x = T.root;
while (x != NULL) {
y = x;
if (x.key > key) {
x = x.left;
} else {
x = x.right;
}
}
if (y == NULL) {
T.root = node;
node->color = BLACK; // 根節點爲黑色
return;
} else if (y->key >= key) {
y.left = node;
} else {
y.right = node;
}
node->color = RED; // 初始顏色爲紅
RbTreeInsertFixup(T, node);
}
void RbTreeInsertFixup(RbTree *T, TreeNode *node) {
while (node != NULL && node->p != NULL && node->p->color == RED) {
TreeNode *p = node->p;
TreeNode *gp = node->p->p; // 因爲p是紅色的,不可能是根節點,所以多一定存在gp。
if (gp->left == p) { // Lxx
if (gp->right != NULL && gp->right->color == RED) { // LLr and LRr
gp->color = RED;
p->color = BLACK;
gp->right->color = BLACK;
node = gp;
} else {
if (p->right == node) { // LRb
LeftRotate(T, p); // turn LRb to LLb
node = p; // parent becomes the node
p = node->p; // node becomes the parent
}
// LLb
gp->color = RED;
p->color = BLACK;
RightRotate(T, gp);
}
} else { // Rxx
if (gp->left != NULL && gp->left->color == RED) { // RLr and RRr
gp->left->color = BLACK;
p->color = BLACK;
gp->color = RED;
node = gp;
} else {
if (p->left == node) { // RLb
RightRotate(T, p); // turn RLb to RRb
node = p; // exchange the node and p
p = node->p;
}
// RRb
gp->color = RED;
node->color = BLACK;
LeftRotate(T, gp);
}
}
}
T.root.color = BLACK;
}
2.3. 刪除
紅黑樹刪除節點的過程是基於二叉搜索樹的刪除過程,根據刪除節點的情況具體分析,其實刪除的過程從樹的結構上看(不考慮衛星數據和key)本質是消失了一個葉節點或者一個只有一個孩子的節點。
- 刪除葉節點或者單孩子節點(被刪除消失)
從上圖看如果刪除節點本身就是葉子節點或者只有一個孩子的節點,那麼必然符合上述論斷。 - 刪除內部節點(被移動導致消失)
如果刪除內部節點,從上圖看其實最後還是將一個前驅或者後繼移到該內部節點,從結構上相當於刪除了前驅或者後繼節點,一定是葉子或者單孩子節點。
所以二叉搜索樹的刪除本質都是刪除了一個葉子節點或只有一個孩子的節點,因爲最後樹中消失的都是這類節點,如圖中的虛線圈出的節點。
2.3.1. 調整分析
根據以上分析知道刪除最後本質是消失了一個葉子節點或者單孩子節點(也就是導致了這類節點在樹結構上的消失,不是數據的消失),那麼樹的不平衡只能是這類節點消失引起的。
假設消失的節點是y,替補y的是y的孩子,假設爲x。
-
I. y爲紅色
消失的節點y是紅色,無論是被刪除還是被移動,消失的紅色的節點只可能是葉子節點(如果有孩子,孩子黑色,黑高不一致違反條件,如果孩子紅色那麼違反連續無連續紅色節點條件)。那麼紅色葉節點消失對紅黑樹結構無影響,無須調整。 -
II. y爲黑色
消失的節點y是黑色,消失後所在分支黑高減一,此時會有節點x填補空缺,如果需要保證整棵樹仍然滿足紅黑樹,那麼就需要往x所在分支上再加一個黑高(保證黑高不變)。如果x是紅色直接加,如果x是黑色,可以分爲兩種方法,一種是將問題向上面拋,一種是在x側加上一個黑色節點,即向叔分支借多的紅色(一種顏色轉移,不是真的借節點)。
總結
只有在樹結構中消失的節點(因刪除或者移動消失)爲黑色時才需要進行調整。如果消失的節點是紅色無須調整,見上圖分析。
2.3.2. 調整分類
前面分析知道只有消失的節點是黑色時,黑高減一需要調整,調整可根據填充節點x的顏色分爲兩大類,紅色或者黑色。
2.3.2.1. x是紅色節點
直接將x變成黑色節點即可增加黑高,填補y消失引起的黑高減一,樹平衡。
2.3.2.2. x是黑色
x是黑色節點,那麼無法把黑色繼續加在該節點上來增加黑高,但可以嘗試兩種方案,方法一:給父節點p加黑,方法二:在x側加上一個黑色節點。根據兄弟節點顏色進行分類討論兩種方案可行性,一般給父節點加黑操作複雜一些(涉及遞歸),所以討論時優先考慮x側加黑節點。有如下結論
結論:
兄弟爲紅時兩種方法均不可實施,只可以轉爲兄弟爲黑來進行操作,兄弟爲黑時,如果兩個黑孩子,則只能給父節點加黑,如果有不變孩子爲紅,則可以進行一次旋轉來對x側加黑節點,如果不變孩子爲黑,則進行兩次旋轉即可在x側加黑節點。
假設x的兄弟b,父親爲p。
不變孩子,即旋轉後仍爲其孩子的孩子,例如左旋則是右孩子,因爲旋轉後右孩子仍然爲其右孩子,右旋則是左孩子,相應的變孩子就是旋轉時會丟掉的孩子。
-
兄弟爲紅(Lr、Rr)
- 方法一:x側加黑節點
因爲在x側加黑節點只能通過旋轉來完成,如果兄弟b爲紅,那麼旋轉後兄弟b爲祖父,必須繼續保持黑,此時兄弟的一個孩子會掛在x的父親節點p上,這導致b的孩子節點的黑高加一,因爲原來該子的父親是紅的,旋轉之後雖然祖父不變,但是父親顏色變成黑了,不可行。 - 方法二:父節點加黑
父節點加黑需要兄弟b移除一個黑高,由於b是紅色,移除黑高需要從子樹開始,較難實施。因爲從黑高的定義來看,都是從根向下的,如果要減則必須b的左右子樹同時減黑高,如果左右子樹剛好沒有多餘黑色節點(比如黑色節點在紅色節點中間,無法刪除,否則出現連續黑),則無法操作。
總結:兩種方案均無法實施,只可以轉爲黑色,而一次旋轉既可轉換。
- 方法一:x側加黑節點
-
兄弟爲黑,不變孩子爲紅(LbI、RbI)
- 方法一:
不變孩子爲紅,變孩子無約束。x側加黑節點,假設x在左側,那麼b在右側,在p上進行左旋,然後使得b爲p的顏色,p變爲黑,此時x側成功消除影響,b右孩子此時就是不變孩子,此時右孩子是紅色,爲了消除影響,直接讓右孩子變成紅色,即可消除旋轉導致b右側黑高減一的影響。 - 方法二:
無法實施,不可以讓兄弟變紅,因爲有孩子爲紅,且無法對子樹進行減一操作,因爲可能子樹無法減去黑色節點。
總結:不變孩子爲紅,進行單次旋轉即可。
- 方法一:
-
兄弟爲黑,不變孩子爲黑,變孩子爲紅(LbII、RbII)
- 方法一:
此時和上面分析類似,不變孩子爲黑,那麼另一個孩子必須是紅,不然就成孩子全黑了。可以通過在b上進行旋轉。假設x在左側,則在b上進行右旋,那麼b的左孩子是變孩子,旋轉前讓b變成紅色,b的左孩子變成黑色,旋轉後b的左孩子是x的兄弟。此時x的新兄弟b的不變孩子也就是原來的b是紅色的,變成了LbI型的情況,根據以上分析知道在p上進行一次左旋即可。 - 方法二:
無法實施,因爲存在紅孩子。
- 方法一:
總結 不變孩子爲黑,變孩子爲紅,進行單次旋轉轉爲不變孩子爲紅的情況,再進一次旋轉即可。
-
兄弟爲黑,其孩子都爲黑(LbIII、RBIII)
- 方法一:x側加黑節點
假設x在b左側,因爲孩子均爲黑,如果在p上左旋讓x側加黑,則p和b交換顏色,然後左旋,會導致兄弟b右側子樹的黑高減一,因爲b必須填補p的顏色,導致黑色節點數減一,且無法增加黑色節點,因爲b的孩子均爲黑,不易操作,且如果子樹都是滿黑色節點更無法增加。如果在b上進行操作,因爲左孩子爲黑,旋轉後左孩子的黑高會減一,不易操作。對於x在b的右側情況類似。 - 方法二:父節點加黑
則令x=p,遞歸向上調整直到遇到紅色節點停。如果給父節點增加一層黑色(準確說是祖先節點,因爲如果父節點爲黑,黑色會增加給祖先),會導致x兄弟分支會多出一個黑高,所以給父節點增加一層黑色需要兄弟需要移除一個黑高,以保證父節點增加黑高時(更確切的說是祖先節點增加黑高時)兄弟分支所有節點黑高不變,因爲已經移除了一個黑高,所以路徑上方增加一個黑高不會影響自己的分支。方案可行,只要將兄弟b的顏色改爲紅即可,因爲孩子都是黑的,無影響。
總結 只可以使用給父節點加黑的操作,然後可能需要遞歸,如果p剛好是紅則不需要,如果是黑則需要遞歸往上,令x=p。
- 方法一:x側加黑節點
2.3.3. 調整
-
兄弟爲紅
此時根據雙重黑節點x在b的左還是右分Lr型和Rr型,分析Lr,另外一種鏡像對稱。- Lr
圖2.4 所以對Lr來分析。y爲黑,x是黑,b紅色,所以p必須是黑色,且b必須有兩個孩子是黑色,因爲從p的黑高來分析之前y爲黑色,p的黑高爲2,那麼b是紅,必須保證b的黑高爲1,所以其必須有兩個黑孩子。進行一次左旋,將p變爲紅,b變爲黑,而後成爲x的兄弟節點,令,那麼問題轉爲b是黑的情況
-
兄弟爲黑
x在b的左右分爲兩種大類,根據不變孩子的顏色分爲I、II、III型,不變孩子爲紅則是I型,不變孩子黑且變孩子紅II型,孩子均爲黑時III型。分析LbI、LbII、LbIII,另外種RbI、RbII、RbIII對稱。-
LbI
圖2.5 對於LbI型,只需要在p上進行左旋,然後將p變成黑色,b變成p原先的顏色,然後b不變孩子即右孩子變成黑色即可。圖中陰影表示可能是紅色,可能是黑色。 -
LbII
圖2.6 對於LbII型,只需要在b上進行右旋,然後將b變成紅色,b的左孩子變成黑色,圖中陰影表示可能是紅色,可能是黑色,空白是紅色,黑色即黑色,旋轉後右邊圈出部分其實爲LbI型,進一步按LbI型操作即可。 -
LbIII
圖2.7 對於LBIII型,將b變成紅色,然後令x=p,即使得x向上遷移(本質是嘗試對祖先加黑),那麼根據新的x可繼續按照以上所列類型來處理。例如如果新x是黑色,那麼查看兄弟節點,如果新x是紅色,立即上色結束。
-
個人總結
對於刪除時,如果消失的節點、替代的節點、兄弟節點、不變孩子是紅色的都是容易解決掉的,但是如果是黑色的就不好解決。
2.3.4. 實現
注意:本實現和算法導論中的實現不一樣,使用了NULL,而非T.nil,如果和算法導論中的fixup保持一致會導致調整時傳入了NULL節點則無法調整,本實現採取的方式是零時生成一個nil節點(無意義的哨兵節點,但是可以保證程序繼續跑),而算法導論中是通過T.nil哨兵來記錄的,在算法導論中的RB-TransPlant當中,如果涉及嫁接,那麼會臨時將T.nil哨兵的p指到nil的父。也就是說雖然T.nil只有一個,被所有葉節點所指,但是他卻可以臨時記錄最後操作的nil是樹中哪個葉節點,因爲T.nil->p = leaf;
便可記錄,這是算法導論版本的T.nil精巧的地方。
#define RED 0
#define BLACK 1
void RbTreeTransplant(RbTree *T, TreeNode *x, TreeNode *y);
void RbTreeDeleteFixup(RbTree *T, TreeNode *p, TreeNode *x);
void RbTreeDelete(RbTree *T, ket_t key);
void RbTreeRotate(TreeNode *x);
void RbTreeRightRotate(RbTree *T, TreeNode *x) {
if (T == NULL || x == NULL) return;
TreeNode *y = x->left;
if (y == NULL) return;
if (x->p == NULL) {
T.root = x->left;
} else if (x->p->left == x){ // x is left child
x->p->left = y;
} else { // x is right child
x->p->right = y;
}
y->p = x->p;
x->left = y->right;
if (y->right != NULL) {
x->left->p = x;
}
y->right = x;
x->p = y;
}
void RbTreeLeftRotate(RbTree *T, TreeNode * x) {
if (T == NULL || x == NULL) return;
TreeNode *y = x->right;
if (y == NULL) return;
if (x->p == NULL) {
T.root = y;
} else if (x->p->left == x) {
x->p->left = y;
} else {
x->p->right = y;
}
y->p = x->p;
x->right = y->left;
if (y->left != NULL) {
x->right->p = x;
}
y->left = x;
x->p = y;
}
// 把任何一顆子樹(包括空)嫁接到一棵樹上某個節點上,不可嫁接到空的節點
void RbTreeTransplant(RbTree *T, TreeNode *x, TreeNode *y) {
if (T == NULL || x == NULL) return; // 不可以將子樹嫁接到一個空節點或者空樹上
if (x->p == NULL) { 嫁接整棵樹,其實就是移栽了
T.root = y;
} else if (x->p->left == p) {
x->p->left = y;
} else {
x->p->right = y;
}
if (y != NULL) {
y->p = x->p;
}
}
void RbTreeDelete(RbTree *T, key_t key) {
TreeNode *node = BSTSearch(T, key);
TreeNode *p = NULL; // 如果x是空節點,那麼必須要記錄刪除後其父親節點
if (node == NULL) return;
color_t disappear_node_color = node->color; // 記錄消失節點的顏色,因爲默認消失的是刪除節點
TreeNode *x = NULL; // 記錄替換的節點;
if (node->left == NULL) { // 消失的節點時被刪除的,右子樹嫁接到自己的位置,即替換節點x是右孩子, 可能是空
RbTreeTransplant(T, node, node->right);
x = node->right;
if (x == NULL) {
p = node->p;
}
} else if (node->right == NULL) { // 消失節點是被刪除節點,嫁接左孩子,即x是左孩子,可能是空
RbTreeTransplant(T, node, node->left);
x = node->left;
if (x == NULL) {
p = node->p;
}
} else { // 消失節點是被移動的後繼,也就是刪除節點是有兩個孩子的節點
TreeNode *successor = RbTreeMinium(node->right); // 一定存在,如果不存在那麼右孩子爲空不會運行到這裏
disappear_node_color = successor.color; // 需要重新記錄,因爲默認消失的是刪除節點,但是目前發現不是
x = successor.right; // 替換節點x是y的右孩子,可以爲空
if (successor->p != node) { // 後繼剛好是刪除節點的兒子時,無須移植過程
RbTreeTransplant(T, successor, x);
if (x == NULL) {
p = successor->p;
}
successor->right = node->right;
successor->right->p = successor;
} else {
if (x == NULL) {
p = successor;
}
}
RbTreeTransplant(T,node,successor);
successor->left = node->left;
successor->left->p = successor;
successor->color = node->color;
}
if (disappear_node_color == BLACK) {
RbTreeDeleteFixup(T, p, x);
}
}
void RbTreeDeletFixup(RbTree *T, TreeNode *px, TreeNode *sx) {
if (sx == NULL) { // sx如果是空,則生成臨時的nil節點,且讓px指向
sx = new TreeNode();
sx->p = px;
sx->color = BLACK;
if(px->left == NULL) { // px只可能有一個null,因爲只有px兒子是黑色,纔會調整,px之前的兒子是黑色,另外一邊必須有節點,否則不滿足黑高
px->left = sx;
} else {
px->right = sx;
}
}
// 以上目的是爲了防止sx是空,無法找到其父節點,參數px也是據此所額外傳遞的參數
TreeNode *x = sx;
while (x->p != NULL && x->color == BLACK) {
TreeNode *p = x->p;
if (p->left == x) { // Left
TreeNode *b = p->right;
if (b->color == RED) { // Lr
RbTreeLeftRotate(T, p);
p->color = RED;
b->color = BLACK;
} else {
if ((b->right != NULL && b->right->color == BLACK) && (b- >left != NULL && b->left->color == BLACK) || (b->left == NULL && b->right == NULL)) { // LbIII, 不是空就全黑,或者全空
b->color = RED;
x = p;
} else if (b->right != NULL && b->right->color == BLACK) { // LbII 不變孩子爲黑,但是變孩子爲紅,因爲不是兩個全黑,否則走上面的
RbTreeRightRotate(T, b);
b->color = RED;
b->p->color = BLACK;
b = b->p; // 轉爲LbI,走下一輪循環處理最後一種情況後結束
} else {
// LbI
RbTreeLeftRotate(T, p);
b->color = p->color;
p->color = BLACK;
b->right->color = BLACK;
x = T.root; // 結束
}
}
} else {
TreeNode *b = p->left;
if (b->color == RED) { // Rr
RbTreeRightRotate(T, p);
p->color = RED;
b->color = BLACK;
} else {
// RbIII
if ((b->right != NULL && b->right->color == BLACK) && (b- >left != NULL && b->left->color == BLACK) || (b->left == NULL && b->right == NULL)) { // RbIII, 不是空就全黑,或者全空
b->color = RED;
x = p;
} else if (b->left == NULL || b->left->color == BLACK) {
RbTreeLeftRotate(T, b);
b->color = RED; // 着色爲紅
b = b->p; // 讓b上移
b->color = BLACK; // 新b需要着色爲黑
} else {
RbTreeRightRotate(T, p);
b->color = p->color; // 改變b的顏色爲p的顏色
p->color = BLACK; // p的顏色變黑
b->left->color = BLACK; // b左孩子節點需要變成黑色
x = T.root; // 結束
}
}
}
}
x->color = BLACK;
// 消除用nil節點代替NULL的動作帶來的影響。
if (px->left == sx) {
px->left == NULL;
} else {
px->right = NULL;
}
}