教你初步了解紅黑樹
教你透徹了解紅黑樹
二叉查找樹
由於紅黑樹本質上就是一棵二叉查找樹,所以在了解紅黑樹之前,咱們先來看下二叉查找樹。
二叉查找樹(Binary Search Tree),也稱有序二叉樹(ordered binary tree),排序二叉樹(sorted binary tree),是指一棵空樹或者具有下列性質的二叉樹:
- 若任意結點的左子樹不空,則左子樹上所有結點的值均小於它的根結點的值;
- 若任意結點的右子樹不空,則右子樹上所有結點的值均大於它的根結點的值;
- 任意結點的左、右子樹也分別為二叉查找樹。
- 沒有鍵值相等的結點(no duplicate nodes)。
因為,一棵由n個結點,隨機構造的二叉查找樹的高度為lgn,所以順理成章,一般操作的執行時間為O(lgn).(至於n個結點的二叉樹高度為lgn的證明,可參考算法導論 第12章 二叉查找樹 第12.4節)。
但二叉樹若退化成了一棵具有n個結點的線性鏈後,則此些操作最壞情況運行時間為O(n)。後面我們會看到一種基於二叉查找樹-紅黑樹,它通過一些性質使得樹相對平衡,使得最終查找、插入、刪除的時間復雜度最壞情況下依然為O(lgn)。
紅黑樹
前面我們已經說過,紅黑樹,本質上來說就是一棵二叉查找樹,但它在二叉查找樹的基礎上增加了著色和相關的性質使得紅黑樹相對平衡,從而保證了紅黑樹的查找、插入、刪除的時間復雜度最壞為O(log n)。
但它是如何保證一棵n個結點的紅黑樹的高度始終保持在h = logn的呢?這就引出了紅黑樹的5條性質:
1)每個結點要麽是紅的,要麽是黑的。 2)根結點是黑的。 3)每個葉結點(葉結點即指樹尾端NIL指針或NULL結點)是黑的。 4)如果一個結點是紅的,那麽它的倆個兒子都是黑的。 5)對於任一結點而言,其到葉結點樹尾端NIL指針的每一條路徑都包含相同數目的黑結點。
正是紅黑樹的這5條性質,使得一棵n個結點是紅黑樹始終保持了logn的高度,從而也就解釋了上面我們所說的“紅黑樹的查找、插入、刪除的時間復雜度最壞為O(log n)”這一結論的原因。
如下圖所示,即是一顆紅黑樹(下圖引自wikipedia:http://t.cn/hgvH1l):
上文中我們所說的 "葉結點" 或"NULL結點",它不包含數據而只充當樹在此結束的指示,這些結點以及它們的父結點,在繪圖中都會經常被省略。
樹的旋轉知識
當我們在對紅黑樹進行插入和刪除等操作時,對樹做了修改,那麽可能會違背紅黑樹的性質。
為了繼續保持紅黑樹的性質,我們可以通過對結點進行重新著色,以及對樹進行相關的旋轉操作,即修改樹中某些結點的顏色及指針結構,來達到對紅黑樹進行插入或刪除結點等操作後,繼續保持它的性質或平衡。
樹的旋轉,分為左旋和右旋,以下借助圖來做形象的解釋和介紹:
1.左旋
如上圖所示:
當在某個結點pivot上,做左旋操作時,我們假設它的右孩子y不是NIL[T],pivot可以為任何不是NIL[T]的左孩子結點。
左旋以pivot到y之間的鏈為“支軸”進行,它使y成為該孩子樹新的根,而y的左孩子b則成為pivot的右孩子。
左旋操作的參考代碼如下所示(以x代替上述的pivot):
LEFT-ROTATE(T, x)
1 y ← right[x] ? Set y.
2 right[x] ← left[y] ? Turn y‘s left subtree into x‘s right subtree.
3 p[left[y]] ← x
4 p[y] ← p[x] ? Link x‘s parent to y.
5 if p[x] = nil[T]
6 then root[T] ← y
7 else if x = left[p[x]]
8 then left[p[x]] ← y
9 else right[p[x]] ← y
10 left[y] ← x ? Put x on y‘s left.
11 p[x] ← y
2.右旋
右旋與左旋差不多,再此不做詳細介紹。
對於樹的旋轉,能保持不變的只有原樹的搜索性質,而原樹的紅黑性質則不能保持,在紅黑樹的數據插入和刪除後可利用旋轉和顏色重塗來恢復樹的紅黑性質。
紅黑樹的插入
要真正理解紅黑樹的插入和刪除,還得先理解二叉查找樹的插入和刪除。磨刀不誤砍柴工,咱們再來分別了解下二叉查找樹的插入和刪除。
二叉查找樹的插入
如果要在二叉查找樹中插入一個結點,首先要查找到結點插入的位置,然後進行插入,假設插入的結點為z的話,插入的偽代碼如下:
TREE-INSERT(T, z)
1 y ← NIL
2 x ← root[T]
3 while x ≠ NIL
4 do y ← x
5 if key[z] < key[x]
6 then x ← left[x]
7 else x ← right[x]
8 p[z] ← y
9 if y = NIL
10 then root[T] ← z ? Tree T was empty
11 else if key[z] < key[y]
12 then left[y] ← z
13 else right[y] ← z
可以看到,上述第3-7行代碼即是在二叉查找樹中查找z待插入的位置,如果插入結點z小於當前遍歷到的結點,則到當前結點的左子樹中繼續查找,如果z大於當前結點,則到當前結點的右子樹中繼續查找,第9-13行代碼找到待插入的位置,如果z依然比此刻遍歷到的新的當前結點小,則z作為當前結點的左孩子,否則作為當前結點的右孩子。
紅黑樹的插入和插入修復
現在我們了解了二叉查找樹的插入,接下來,咱們便來具體了解紅黑樹的插入操作。紅黑樹的插入相當於在二叉查找樹插入的基礎上,為了重新恢復平衡,繼續做了插入修復操作。
假設插入的結點為z,紅黑樹的插入偽代碼具體如下所示:
RB-INSERT(T, z)
1 y ← nil[T]
2 x ← root[T]
3 while x ≠ nil[T]
4 do y ← x
5 if key[z] < key[x]
6 then x ← left[x]
7 else x ← right[x]
8 p[z] ← y
9 if y = nil[T]
10 then root[T] ← z
11 else if key[z] < key[y]
12 then left[y] ← z
13 else right[y] ← z
14 left[z] ← nil[T]
15 right[z] ← nil[T]
16 color[z] ← RED
17 RB-INSERT-FIXUP(T, z)
我們把上面這段紅黑樹的插入代碼,跟我們之前看到的二叉查找樹的插入代碼,可以看出,RB-INSERT(T, z)前面的第1-13行代碼基本就是二叉查找樹的插入代碼,然後第14-16行代碼把z的左孩子、右孩子都賦為葉結點nil,再把z結點著為紅色,最後為保證紅黑性質在插入操作後依然保持,調用一個輔助程序RB-INSERT-FIXUP來對結點進行重新著色,並旋轉。
換言之
- 如果插入的是根結點,因為原樹是空樹,此情況只會違反性質2,所以直接把此結點塗為黑色。
- 如果插入的結點的父結點是黑色,由於此不會違反性質2和性質4,紅黑樹沒有被破壞,所以此時也是什麽也不做。
但當遇到下述3種情況時:
- 插入修復情況1:如果當前結點的父結點是紅色且祖父結點的另一個子結點(叔叔結點)是紅色
- 插入修復情況2:當前結點的父結點是紅色,叔叔結點是黑色,當前結點是其父結點的右子
- 插入修復情況3:當前結點的父結點是紅色,叔叔結點是黑色,當前結點是其父結點的左子
又該如何調整呢?答案就是根據紅黑樹插入代碼RB-INSERT(T, z)最後一行調用的RB-INSERT-FIXUP(T,z)所示操作進行,具體如下所示:
RB-INSERT-FIXUP(T,z)
1 while color[p[z]] = RED
2 do if p[z] = left[p[p[z]]]
3 then y ← right[p[p[z]]]
4 if color[y] = RED
5 then color[p[z]] ← BLACK ? Case 1
6 color[y] ← BLACK ? Case 1
7 color[p[p[z]]] ← RED ? Case 1
8 z ← p[p[z]] ? Case 1
9 else if z = right[p[z]]
10 then z ← p[z] ? Case 2
11 LEFT-ROTATE(T, z) ? Case 2
12 color[p[z]] ← BLACK ? Case 3
13 color[p[p[z]]] ← RED ? Case 3
14 RIGHT-ROTATE(T, p[p[z]]) ? Case 3
15 else (same as then clause
with "right" and "left" exchanged)
16 color[root[T]] ← BLACK
下面,咱們來分別處理上述3種插入修復情況。
插入修復情況1:當前結點的父結點是紅色且祖父結點的另一個子結點(叔叔結點)是紅色。
即如下代碼所示:
1 while color[p[z]] = RED
2 do if p[z] = left[p[p[z]]]
3 then y ← right[p[p[z]]]
4 if color[y] = RED
此時父結點的父結點一定存在,否則插入前就已不是紅黑樹。
與此同時,又分為父結點是祖父結點的左子還是右子,對於對稱性,我們只要解開一個方向就可以了。
在此,我們只考慮父結點為祖父左子的情況。
同時,還可以分為當前結點是其父結點的左子還是右子,但是處理方式是一樣的。我們將此歸為同一類。
對策:將當前結點的父結點和叔叔結點塗黑,祖父結點塗紅,把當前結點指向祖父結點,從新的當前結點重新開始算法。即如下代碼所示:
5 then color[p[z]] ← BLACK ? Case 1
6 color[y] ← BLACK ? Case 1
7 color[p[p[z]]] ← RED ? Case 1
8 z ← p[p[z]] ? Case 1
針對情況1,變化前(圖片來源:saturnman)[當前結點為4結點]:
變化後:
插入修復情況2:當前結點的父結點是紅色,叔叔結點是黑色,當前結點是其父結點的右子
對策:當前結點的父結點做為新的當前結點,以新當前結點為支點左旋。即如下代碼所示:
9 else if z = right[p[z]]
10 then z ← p[z] ? Case 2
11 LEFT-ROTATE(T, z) ? Case 2
如下圖所示,變化前[當前結點為7結點]:
變化後:
插入修復情況3:當前結點的父結點是紅色,叔叔結點是黑色,當前結點是其父結點的左子
解法:父結點變為黑色,祖父結點變為紅色,在祖父結點為支點右旋,操作代碼為:
12 color[p[z]] ← BLACK ? Case 3
13 color[p[p[z]]] ← RED ? Case 3
14 RIGHT-ROTATE(T, p[p[z]]) ? Case 3
最後,把根結點塗為黑色,整棵紅黑樹便重新恢復了平衡。
如下圖所示[當前結點為2結點]
變化後:
紅黑樹的刪除
ok,接下來,咱們最後來了解,紅黑樹的刪除操作。
"我們刪除的結點的方法與常規二叉搜索樹中刪除結點的方法是一樣的,如果被刪除的結點不是有雙非空子女,則直接刪除這個結點,用它的唯一子結點頂替它的位置,如果它的子結點都是空結點,那就用空結點頂替它的位置,如果它的雙子全為非空,我們就把它的直接後繼結點內容復制到它的位置,之後以同樣的方式刪除它的後繼結點,它的後繼結點不可能是雙子非空,因此此傳遞過程最多只進行一次。”
二叉查找樹的刪除
繼續講解之前,補充說明下二叉樹結點刪除的幾種情況,待刪除的結點按照兒子的個數可以分為三種:
- 沒有兒子,即為葉結點。直接把父結點的對應兒子指針設為NULL,刪除兒子結點就OK了。
- 只有一個兒子。那麽把父結點的相應兒子指針指向兒子的獨生子,刪除兒子結點也OK了。
- 有兩個兒子。這是最麻煩的情況,因為你刪除結點之後,還要保證滿足搜索二叉樹的結構。其實也比較容易,我們可以選擇左兒子中的最大元素或者右兒子中的最小元素放到待刪除結點的位置,就可以保證結構的不變。當然,你要記得調整子樹,畢竟又出現了結點刪除。習慣上大家選擇左兒子中的最大元素,其實選擇右兒子的最小元素也一樣,沒有任何差別,只是人們習慣從左向右。這裏咱們也選擇左兒子的最大元素,將它放到待刪結點的位置。左兒子的最大元素其實很好找,只要順著左兒子不斷的去搜索右子樹就可以了,直到找到一個沒有右子樹的結點。那就是最大的了。
二叉查找樹的刪除代碼如下所示:
TREE-DELETE(T, z)
1 if left[z] = NIL or right[z] = NIL
2 then y ← z
3 else y ← TREE-SUCCESSOR(z)
4 if left[y] ≠ NIL
5 then x ← left[y]
6 else x ← right[y]
7 if x ≠ NIL
8 then p[x] ← p[y]
9 if p[y] = NIL
10 then root[T] ← x
11 else if y = left[p[y]]
12 then left[p[y]] ← x
13 else right[p[y]] ← x
14 if y ≠ z
15 then key[z] ← key[y]
16 copy y‘s satellite data into z
17 return y
紅黑樹的刪除和刪除修復
OK,回到紅黑樹上來,紅黑樹結點刪除的算法實現是:
RB-DELETE(T, z) 單純刪除結點的總操作
1 if left[z] = nil[T] or right[z] = nil[T]
2 then y ← z
3 else y ← TREE-SUCCESSOR(z)
4 if left[y] ≠ nil[T]
5 then x ← left[y]
6 else x ← right[y]
7 p[x] ← p[y]
8 if p[y] = nil[T]
9 then root[T] ← x
10 else if y = left[p[y]]
11 then left[p[y]] ← x
12 else right[p[y]] ← x
13 if y ≠ z
14 then key[z] ← key[y]
15 copy y‘s satellite data into z
16 if color[y] = BLACK
17 then RB-DELETE-FIXUP(T, x)
18 return y
“在刪除結點後,原紅黑樹的性質可能被改變,如果刪除的是紅色結點,那麽原紅黑樹的性質依舊保持,此時不用做修正操作,如果刪除的結點是黑色結點,原紅黑樹的性質可能會被改變,我們要對其做修正操作。那麽哪些樹的性質會發生變化呢,如果刪除結點不是樹唯一結點,那麽刪除結點的那一個支的到各葉結點的黑色結點數會發生變化,此時性質5被破壞。如果被刪結點的唯一非空子結點是紅色,而被刪結點的父結點也是紅色,那麽性質4被破壞。如果被刪結點是根結點,而它的唯一非空子結點是紅色,則刪除後新根結點將變成紅色,違背性質2。”
RB-DELETE-FIXUP(T, x) 恢復與保持紅黑性質的工作
1 while x ≠ root[T] and color[x] = BLACK
2 do if x = left[p[x]]
3 then w ← right[p[x]]
4 if color[w] = RED
5 then color[w] ← BLACK ? Case 1
6 color[p[x]] ← RED ? Case 1
7 LEFT-ROTATE(T, p[x]) ? Case 1
8 w ← right[p[x]] ? Case 1
9 if color[left[w]] = BLACK and color[right[w]] = BLACK
10 then color[w] ← RED ? Case 2
11 x ← p[x] ? Case 2
12 else if color[right[w]] = BLACK
13 then color[left[w]] ← BLACK ? Case 3
14 color[w] ← RED ? Case 3
15 RIGHT-ROTATE(T, w) ? Case 3
16 w ← right[p[x]] ? Case 3
17 color[w] ← color[p[x]] ? Case 4
18 color[p[x]] ← BLACK ? Case 4
19 color[right[w]] ← BLACK ? Case 4
20 LEFT-ROTATE(T, p[x]) ? Case 4
21 x ← root[T] ? Case 4
22 else (same as then clause with "right" and "left" exchanged)
23 color[x] ← BLACK
“上面的修復情況看起來有些復雜,下面我們用一個分析技巧:我們從被刪結點後來頂替它的那個結點開始調整,並認為它有額外的一重黑色。這裏額外一重黑色是什麽意思呢,我們不是把紅黑樹的結點加上除紅與黑的另一種顏色,這裏只是一種假設,我們認為我們當前指向它,因此空有額外一種黑色,可以認為它的黑色是從它的父結點被刪除後繼承給它的,它現在可以容納兩種顏色,如果它原來是紅色,那麽現在是紅+黑,如果原來是黑色,那麽它現在的顏色是黑+黑。有了這重額外的黑色,原紅黑樹性質5就能保持不變。現在只要恢復其它性質就可以了,做法還是盡量向根移動和窮舉所有可能性。"--saturnman。
如果是以下情況,恢復比較簡單:
- a)當前結點是紅+黑色
解法,直接把當前結點染成黑色,結束此時紅黑樹性質全部恢復。 - b)當前結點是黑+黑且是根結點, 解法:什麽都不做,結束。
但如果是以下情況呢?:
- 刪除修復情況1:當前結點是黑+黑且兄弟結點為紅色(此時父結點和兄弟結點的子結點分為黑)
- 刪除修復情況2:當前結點是黑加黑且兄弟是黑色且兄弟結點的兩個子結點全為黑色
- 刪除修復情況3:當前結點顏色是黑+黑,兄弟結點是黑色,兄弟的左子是紅色,右子是黑色
- 刪除修復情況4:當前結點顏色是黑-黑色,它的兄弟結點是黑色,但是兄弟結點的右子是紅色,兄弟結點左子的顏色任意
此時,我們需要調用RB-DELETE-FIXUP(T, x),來恢復與保持紅黑性質的工作。
下面,咱們便來分別處理這4種刪除修復情況。
刪除修復情況1:當前結點是黑+黑且兄弟結點為紅色(此時父結點和兄弟結點的子結點分為黑)。
解法:把父結點染成紅色,把兄弟結點染成黑色,之後重新進入算法(我們只討論當前結點是其父結點左孩子時的情況)。此變換後原紅黑樹性質5不變,而把問題轉化為兄弟結點為黑色的情況(註:變化前,原本就未違反性質5,只是為了把問題轉化為兄弟結點為黑色的情況)。 即如下代碼操作:
//調用RB-DELETE-FIXUP(T, x) 的1-8行代碼
1 while x ≠ root[T] and color[x] = BLACK
2 do if x = left[p[x]]
3 then w ← right[p[x]]
4 if color[w] = RED
5 then color[w] ← BLACK ? Case 1
6 color[p[x]] ← RED ? Case 1
7 LEFT-ROTATE(T, p[x]) ? Case 1
8 w ← right[p[x]] ? Case 1
變化前:
變化後:
刪除修復情況2:當前結點是黑加黑且兄弟是黑色且兄弟結點的兩個子結點全為黑色。
解法:把當前結點和兄弟結點中抽取一重黑色追加到父結點上,把父結點當成新的當前結點,重新進入算法。(此變換後性質5不變),即調用RB-INSERT-FIXUP(T, z) 的第9-10行代碼操作,如下:
//調用RB-DELETE-FIXUP(T, x) 的9-11行代碼
9 if color[left[w]] = BLACK and color[right[w]] = BLACK
10 then color[w] ← RED ? Case 2
11 x p[x] ? Case 2
變化前:
變化後:
刪除修復情況3:當前結點顏色是黑+黑,兄弟結點是黑色,兄弟的左子是紅色,右子是黑色。
解法:把兄弟結點染紅,兄弟左子結點染黑,之後再在兄弟結點為支點解右旋,之後重新進入算法。此是把當前的情況轉化為情況4,而性質5得以保持,即調用RB-INSERT-FIXUP(T, z) 的第12-16行代碼,如下所示:
//調用RB-DELETE-FIXUP(T, x) 的第12-16行代碼
12 else if color[right[w]] = BLACK
13 then color[left[w]] ← BLACK ? Case 3
14 color[w] ← RED ? Case 3
15 RIGHT-ROTATE(T, w) ? Case 3
16 w ← right[p[x]] ? Case 3
變化前:
變化後:
刪除修復情況4:當前結點顏色是黑-黑色,它的兄弟結點是黑色,但是兄弟結點的右子是紅色,兄弟結點左子的顏色任意。
解法:把兄弟結點染成當前結點父結點的顏色,把當前結點父結點染成黑色,兄弟結點右子染成黑色,之後以當前結點的父結點為支點進行左旋,此時算法結束,紅黑樹所有性質調整正確,即調用RB-INSERT-FIXUP(T, z)的第17-21行代碼,如下所示:
//調用RB-DELETE-FIXUP(T, x) 的第17-21行代碼
17 color[w] ← color[p[x]] ? Case 4
18 color[p[x]] ← BLACK ? Case 4
19 color[right[w]] ← BLACK ? Case 4
20 LEFT-ROTATE(T, p[x]) ? Case 4
21 x ← root[T] ? Case 4
變化前:
變化後:
教你初步了解紅黑樹