學習演算法導論——紅黑樹旋轉插入和刪除
參考:
《演算法導論》
紅黑樹是一棵二叉搜尋樹,每個節點有一個標誌位表示顏色,該顏色可以是紅(RED)或黑(BLACK)。通過對任何一條從根到葉子的簡單路徑上各點的顏色進行約束,就能確保沒有一條路徑會比其他路徑長出2倍,因而是近似於平衡的。
紅黑樹每個節點有5個屬性,color,key,leftChild,rightChild,p。其中color是顏色,key是關鍵字,leftChild是指向左孩子的指標,rightChild是指向右孩子的指標,p是指向父節點的指標。如果一個節點沒有子節點或父節點,則該節點的leftChild,rightChild,p指標指向NIL。這些NIL視為二叉搜尋樹的外部節點,其他節點為內部節點。
一棵紅黑樹有如下性質:
(1)每個節點或是紅色,或是黑色;
(2)根節點是黑色的;
(3)每個葉節點(NIL節點)是黑色的;
(4)如果一個節點是紅色的,則它的兩個子節點都是黑色的;
(5)對每個節點,從該節點到其所有後代葉節點的簡單路徑上,均包含相同數目的黑色節點。
如圖(a)為一棵紅黑樹;
我們可以把NIL看成哨兵T.nil,它的color是黑色,其他屬性任意。如圖(b);
左旋
指標結構的修改是通過旋轉來完成的,這時一種能保持二叉搜尋樹性質的操作。旋轉有左旋和右旋兩個,左旋操作為:
如圖,對x作左旋操作。假設x的右孩子為y(不是NIL),x可以是任何右孩子不為NIL的內部節點,簡單理解為:
以x為中心向左旋轉,交換x,y;α,β,γ順序不變。其中還要注意,每個節點還有指向父節點指標。
動圖:
左旋的虛擬碼:
LEFT-ROTATE(T, x) 01 y ← right[x] // 前提:這裡假設x的右孩子為y。下面開始正式操作 02 right[x] ← left[y] // 將 “y的左孩子” 設為 “x的右孩子”,即 將β設為x的右孩子 03 p[left[y]] ← x // 將 “x” 設為 “y的左孩子的父親”,即 將β的父親設為x 04 p[y] ← p[x] // 將 “x的父親” 設為 “y的父親” 05 if p[x] = nil[T] 06 then root[T] ← y // 情況1:如果 “x的父親” 是空節點,則將y設為根節點 07 else if x = left[p[x]] 08 then left[p[x]] ← y // 情況2:如果 x是它父節點的左孩子,則將y設為“x的父節點的左孩子” 09 else right[p[x]] ← y // 情況3:(x是它父節點的右孩子) 將y設為“x的父節點的右孩子” 10 left[y] ← x // 將 “x” 設為 “y的左孩子” 11 p[x] ← y // 將 “x的父節點” 設為 “y”
c++程式碼:
void Left_Rotate(BiTreeNode* root,BiTreeNode* x)
{
BiTreeNode* y = x->rightChild;
x->rightChild = y->leftChild;
y->leftChild->p = x;
y->p = x->p;
if (x->p == NULL)
root = y;
else if (x->p->leftChild == x)
x->p->leftChild = y;
else
x->p->rightChild = y;
y->leftChild= x;
x->p = y;
}
如上圖,對節點“11”左旋;要注意的是:如果x是根節點,旋轉後y稱為根節點;還有一點,需要知道x是其父節點的左孩子還是右孩子。
右旋
右旋和左旋基本相同:
虛擬碼:
RIGHT-ROTATE(T, y)
01 x ← left[y] // 前提:這裡假設y的左孩子為x。下面開始正式操作
02 left[y] ← right[x] // 將 “x的右孩子” 設為 “y的左孩子”,即 將β設為y的左孩子
03 p[right[x]] ← y // 將 “y” 設為 “x的右孩子的父親”,即 將β的父親設為y
04 p[x] ← p[y] // 將 “y的父親” 設為 “x的父親”
05 if p[y] = nil[T]
06 then root[T] ← x // 情況1:如果 “y的父親” 是空節點,則將x設為根節點
07 else if y = right[p[y]]
08 then right[p[y]] ← x // 情況2:如果 y是它父節點的右孩子,則將x設為“y的父節點的左孩子”
09 else left[p[y]] ← x // 情況3:(y是它父節點的左孩子) 將x設為“y的父節點的左孩子”
10 right[x] ← y // 將 “y” 設為 “x的右孩子”
11 p[y] ← x // 將 “y的父節點” 設為 “x”
c++程式碼:
void Right_Rotate(BiTreeNode* root, BiTreeNode* y)
{
BiTreeNode* x = y->leftChild;
y->leftChild = x->rightChild;
x->rightChild->p = y;
x->p = y->p;
if (y->p == NULL)
root = x;
else if (y->p->rightChild == y)
y->p->rightChild = x;
else
y->p->leftChild = x;
x->rightChild = y;
y->p = x;
}
插入
我們先當紅黑樹為普通的二叉搜尋樹一樣插入節點z,並將其顏色置為RED,然後,為了保證紅黑樹性質,呼叫一個輔助函式來調整紅黑樹。 虛擬碼:RB-INSERT(T, z)
01 y ← nil[T] // 新建節點“y”,將y設為空節點。
02 x ← root[T] // 設“紅黑樹T”的根節點為“x”
03 while x ≠ nil[T] // 找出要插入的節點“z”在二叉樹T中的位置“y”
04 do y ← x
05 if key[z] < key[x]
06 then x ← left[x]
07 else x ← right[x]
08 p[z] ← y // 設定 “z的父親” 為 “y”
09 if y = nil[T]
10 then root[T] ← z // 情況1:若y是空節點,則將z設為根
11 else if key[z] < key[y]
12 then left[y] ← z // 情況2:若“z所包含的值” < “y所包含的值”,則將z設為“y的左孩子”
13 else right[y] ← z // 情況3:(“z所包含的值” >= “y所包含的值”)將z設為“y的右孩子”
14 left[z] ← nil[T] // z的左孩子設為空
15 right[z] ← nil[T] // z的右孩子設為空。至此,已經完成將“節點z插入到二叉樹”中了。
16 color[z] ← RED // 將z著色為“紅色”
17 RB-INSERT-FIXUP(T, z) // 通過RB-INSERT-FIXUP對紅黑樹的節點進行顏色修改以及旋轉,讓樹T仍然是一顆紅黑樹
主要是插入節點後紅黑樹性質的調整函式
虛擬碼:
RB - INSERT - FIXUP(T, z)
01 while color[p[z]] = RED // 若“當前節點(z)的父節點是紅色”,則進行以下處理。
02 if p[z] = left[p[p[z]]] // 若“z的父節點”是“z的祖父節點的左孩子”,則進行以下處理。
03 y ← right[p[p[z]]] // 將y設定為“z的叔叔節點(z的祖父節點的右孩子)”
04 if color[y] = RED // Case 1條件:叔叔是紅色
05 color[p[z]] ← BLACK ? Case 1 // (01) 將“父節點”設為黑色。
06 color[y] ← BLACK ? Case 1 // (02) 將“叔叔節點”設為黑色。
07 color[p[p[z]]] ← RED ? Case 1 // (03) 將“祖父節點”設為“紅色”。
08 z ← p[p[z]] ? Case 1 // (04) 將“祖父節點”設為“當前節點”(紅色節點)
09 else if z = right[p[z]] // Case 2條件:叔叔是黑色,且當前節點是右孩子
10 z ← p[z] ? Case 2 // (01) 將“父節點”作為“新的當前節點”。
11 LEFT - ROTATE(T, z) ? Case 2 // (02) 以“新的當前節點”為支點進行左旋。
12 color[p[z]] ← BLACK ? Case 3 // Case 3條件:叔叔是黑色,且當前節點是左孩子。(01) 將“父節點”設為“黑色”。
13 color[p[p[z]]] ← RED ? Case 3 // (02) 將“祖父節點”設為“紅色”。
14 RIGHT - ROTATE(T, p[p[z]]) ? Case 3 // (03) 以“祖父節點”為支點進行右旋。
15 else (same as then clause with "right" and "left" exchanged) // 若“z的父節點”是“z的祖父節點的右孩子”,將上面的操作中“right”和“left”交換位置,然後依次執行。
16 color[root[T]] ← BLACK
void RB_Insert_FixUp(BiTreeNode* root, BiTreeNode* z)
{
BiTreeNode* y;
while (z->p && z->p->Color == RED)
{
if (z->p == z->p->p->leftChild)
{
y = z->p->p->rightChild;
if (y->Color == RED)
{
z->p->Color = BLACK;
y->Color = BLACK;
z->p->p->Color = RED;
z = z->p->p;
}
else
{
if (z == z->p->rightChild)
{
z = z->p;
Left_Rotate(root, z);
}
z->p->Color = BLACK;
z->p->p->Color = RED;
Right_Rotate(root, z->p->p);
}
}
else
{
y = z->p->p->leftChild;
if (y->Color == RED)
{
z->p->Color = BLACK;
y->Color = BLACK;
z->p->p->Color = RED;
z = z->p->p;
}
else
{
if (z == z->p->leftChild)
{
z = z->p;
Right_Rotate(root, z);
}
z->p->Color = BLACK;
z->p->p->Color = RED;
Left_Rotate(root, z->p->p);
}
}
}
}
刪除
將紅黑樹T內的節點z刪除。需要執行的操作依次是:首先,將T當作一顆二叉樹,將節點刪除;然後,通過RB - DELETE - FIXUP來對節點重新著色並旋轉,以此來保證刪除節點後的樹仍然是一顆紅黑樹。
(01) 將T當作一顆二叉樹,將節點刪除。
這和"刪除常規二叉搜尋樹中刪除節點的方法是一樣的"。分3種情況:
第一種,被刪除節點沒有兒子,即為葉節點。那麼,直接將該節點刪除就OK了。
第二種,被刪除節點只有一個兒子。那麼,直接刪除該節點,並用該節點的唯一子節點頂替它的位置。
第三種,被刪除節點有兩個兒子。那麼,首先把“它的後繼節點的內容”複製給“該節點的內容”;之後,刪除“它的後繼節點”。
這裡有兩點需要說明:第一步中複製時,僅僅複製內容,即將“它的後繼節點的內容”複製給“該節點的內容”。 這相當於用“該節點的後繼節點”取代“該節點”,之後就刪除“該節點的後繼節點”即可,而不需要刪除“該節點”(因為“該節點”已經被“它的後繼節點”所取代)。
第二步中刪除“該節點的後繼節點”時,需要注意:“該節點的後繼節點”不可能是雙子非空,這個根據二叉樹的特性可知。 既然“該節點的後繼節點”不可能雙子都非空,就意味著“該節點的後繼節點”要麼沒有兒子,要麼只有一個兒子。若沒有兒子,則按“第一種”種的辦法進行處理;若只有一個兒子,則按“第二種”中的辦法進行處理。
(02) 通過RB - DELETE - FIXUP來對節點重新著色並旋轉,以此來保證刪除節點後的樹仍然是一顆紅黑樹。
因為(01)中刪除節點之後,可能會違背紅黑樹的特性。所以需要,通過RB - DELETE - FIXUP來重新校正,為當前樹保持紅黑樹的特性。
下面是《演算法導論》中 “從紅黑樹T中刪除節點z”的虛擬碼
RB - DELETE(T, z)
01 if left[z] = nil[T] or right[z] = nil[T]
02 then y ← z // 若“z的左孩子” 或 “z的右孩子”為空,則將“z”賦值給 “y”;
03 else y ← TREE - SUCCESSOR(z) // 否則,將“z的後繼節點”賦值給 “y”。
04 if left[y] ≠ nil[T]
05 then x ← left[y] // 若“y的左孩子” 不為空,則將“y的左孩子” 賦值給 “x”;
06 else x ← right[y] // 否則,“y的右孩子” 賦值給 “x”。
07 p[x] ← p[y] // 將“y的父節點” 設定為 “x的父節點”
08 if p[y] = nil[T]
09 then root[T] ← x // 情況1:若“y的父節點” 為空,則設定“x” 為 “根節點”。
10 else if y = left[p[y]]
11 then left[p[y]] ← x // 情況2:若“y是它父節點的左孩子”,則設定“x” 為 “y的父節點的左孩子”
12 else right[p[y]] ← x // 情況3:若“y是它父節點的右孩子”,則設定“x” 為 “y的父節點的右孩子”
13 if y ≠ z
14 then key[z] ← key[y] // 若“y的值” 賦值給 “z”。注意:這裡只拷貝z的值給y,而沒有拷貝z的顏色!!!
15 copy y's satellite data into z
16 if color[y] = BLACK
17 then RB - DELETE - FIXUP(T, x) // 若“y為黑節點”,則呼叫
18 return y
RB - DELETE - FIXUP(T, x)
01 while x ≠ root[T] and color[x] = BLACK
02 do if x = left[p[x]]
03 then w ← right[p[x]] // 若 “x”是“它父節點的左孩子”,則設定 “w”為“x的叔叔”(即x為它父節點的右孩子)
04 if color[w] = RED // Case 1: x是“黑+黑”節點,x的兄弟節點是紅色。(此時x的父節點和x的兄弟節點的子節點都是黑節點)。
05 then color[w] ← BLACK ? Case 1 // (01) 將x的兄弟節點設為“黑色”。
06 color[p[x]] ← RED ? Case 1 // (02) 將x的父節點設為“紅色”。
07 LEFT - ROTATE(T, p[x]) ? Case 1 // (03) 對x的父節點進行左旋。
08 w ← right[p[x]] ? Case 1 // (04) 左旋後,重新設定x的兄弟節點。
09 if color[left[w]] = BLACK and color[right[w]] = BLACK // Case 2: x是“黑+黑”節點,x的兄弟節點是黑色,x的兄弟節點的兩個孩子都是黑色。
10 then color[w] ← RED ? Case 2 // (01) 將x的兄弟節點設為“紅色”。
11 x ← p[x] ? Case 2 // (02) 設定“x的父節點”為“新的x節點”。
12 else if color[right[w]] = BLACK // Case 3: x是“黑+黑”節點,x的兄弟節點是黑色;x的兄弟節點的左孩子是紅色,右孩子是黑色的。
13 then color[left[w]] ← BLACK ? Case 3 // (01) 將x兄弟節點的左孩子設為“黑色”。
14 color[w] ← RED ? Case 3 // (02) 將x兄弟節點設為“紅色”。
15 RIGHT - ROTATE(T, w) ? Case 3 // (03) 對x的兄弟節點進行右旋。
16 w ← right[p[x]] ? Case 3 // (04) 右旋後,重新設定x的兄弟節點。
17 color[w] ← color[p[x]] ? Case 4 // Case 4: x是“黑+黑”節點,x的兄弟節點是黑色;x的兄弟節點的右孩子是紅色的。(01) 將x父節點顏色 賦值給 x的兄弟節點。
18 color[p[x]] ← BLACK ? Case 4 // (02) 將x父節點設為“黑色”。
19 color[right[w]] ← BLACK ? Case 4 // (03) 將x兄弟節點的右子節設為“黑色”。
20 LEFT - ROTATE(T, p[x]) ? Case 4 // (04) 對x的父節點進行左旋。
21 x ← root[T] ? Case 4 // (05) 設定“x”為“根節點”。
22 else (same as then clause with "right" and "left" exchanged) // 若 “x”是“它父節點的右孩子”,將上面的操作中“right”和“left”交換位置,然後依次執行。
23 color[x] ← BLACK
詳細內容看這裡