17張圖帶你解析紅黑樹的原理!保證你能看懂!

程式設計師追風發表於2020-01-20

二叉查詢樹

由於紅黑樹本質上就是一棵二叉查詢樹,所以在瞭解紅黑樹之前,我們們先來看下二叉查詢樹。

二叉查詢樹(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)”這一結論的原因。

如下圖所示,即是一顆紅黑樹:

 

上文中我們所說的 "葉結點" 或"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作為當前結點的左孩子,否則作為當前結點的右孩子。歡迎大家關注我的公種浩【程式設計師追風】,整理了2019年多家公司java面試題資料100多頁pdf文件,文章都會在裡面更新,整理的資料也會放在裡面。

 

紅黑樹的插入和插入修復

現在我們瞭解了二叉查詢樹的插入,接下來,我們們便來具體瞭解紅黑樹的插入操作。紅黑樹的插入相當於在二叉查詢樹插入的基礎上,為了重新恢復平衡,繼續做了插入修復操作。

假設插入的結點為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,變化前[當前結點為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,接下來,我們們最後來了解,紅黑樹的刪除操作。

"我們刪除的結點的方法與常規二叉搜尋樹中刪除結點的方法是一樣的,如果被刪除的結點不是有雙非空子女,則直接刪除這個結點,用它的唯一子結點頂替它的位置,如果它的子結點都是空結點,那就用空結點頂替它的位置,如果它的雙子全為非空,我們就把它的直接後繼結點內容複製到它的位置,之後以同樣的方式刪除它的後繼結點,它的後繼結點不可能是雙子非空,因此此傳遞過程最多隻進行一次。”

 

二叉查詢樹的刪除

繼續講解之前,補充說明下二叉樹結點刪除的幾種情況,待刪除的結點按照兒子的個數可以分為三種:

  1. 沒有兒子,即為葉結點。直接把父結點的對應兒子指標設為NULL,刪除兒子結點就OK了。

  2. 只有一個兒子。那麼把父結點的相應兒子指標指向兒子的獨生子,刪除兒子結點也OK了。

  3. 有兩個兒子。這是最麻煩的情況,因為你刪除結點之後,還要保證滿足搜尋二叉樹的結構。其實也比較容易,我們可以選擇左兒子中的最大元素或者右兒子中的最小元素放到待刪除結點的位置,就可以保證結構的不變。當然,你要記得調整子樹,畢竟又出現了結點刪除。習慣上大家選擇左兒子中的最大元素,其實選擇右兒子的最小元素也一樣,沒有任何差別,只是人們習慣從左向右。這裡我們們也選擇左兒子的最大元素,將它放到待刪結點的位置。左兒子的最大元素其實很好找,只要順著左兒子不斷的去搜尋右子樹就可以了,直到找到一個沒有右子樹的結點。那就是最大的了。

二叉查詢樹的刪除程式碼如下所示:

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

變化前:

 

變化後:

 

本文參考

本文參考了演算法導論、STL原始碼剖析、計算機程式設計藝術等資料。

 

最後

歡迎大家一起交流,喜歡文章記得點個贊喲,感謝支援!歡迎大家關注我的公種浩【程式設計師追風】,整理了2019年多家公司java面試題資料100多頁pdf文件,文章都會在裡面更新,整理的資料也會放在裡面。

相關文章