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

二叉查找樹

因爲紅黑樹本質上就是一棵二叉查找樹,因此在瞭解紅黑樹以前,我們先來看下二叉查找樹。
二叉查找樹(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做爲當前結點的左孩子,不然做爲當前結點的右孩子。

紅黑樹的插入和插入修復

如今咱們瞭解了二叉查找樹的插入,接下來,我們便來具體瞭解紅黑樹的插入操做。紅黑樹的插入至關於在二叉查找樹插入的基礎上,爲了從新恢復平衡,繼續作了插入修復操做。
假設插入的結點爲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:

當前結點的父結點是紅色且祖父結點的另外一個子結點(叔叔結點)是紅色。java

即以下代碼所示:
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結點]:node


變化後:

插入修復狀況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: 

當前結點是黑加黑且兄弟是黑色且兄弟結點的兩個子結點全爲黑色。bash

解法:把當前結點和兄弟結點中抽取一重黑色追加到父結點上,把父結點當成新的當前結點,從新進入算法。(此變換後性質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:

當前結點顏色是黑+黑,兄弟結點是黑色,兄弟的左子是紅色,右子是黑色。spa

解法:把兄弟結點染紅,兄弟左子結點染黑,以後再在兄弟結點爲支點解右旋,以後從新進入算法。此是把當前的狀況轉化爲狀況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文檔,文章都會在裏面更新,整理的資料也會放在裏面。
相關文章
相關標籤/搜索