1. 程式人生 > >紅黑樹原理,演算法,和構建過程的分析和學習

紅黑樹原理,演算法,和構建過程的分析和學習

參考文章:

紅黑樹原理:此篇邏輯清晰,但是紅黑樹的配圖不行,沒法根據圖來進行實際的操作理解,本文的意圖就是根據作者的思路進行圖片的重新分析。

《演算法導論》中文版,中文版翻譯的馬馬虎虎,但是有些概念翻譯的有點爛,在學習過程中會產生一些疑惑,需要及時更新自己的認知。

紅黑樹特性:

(1)每個節點要麼紅要麼黑。

(2)根節點黑色。

(3)每個葉子節點是黑色。【葉子節點指的是NIL或者NULL的葉子節點】。

(4)如果一個節點是紅色的,那麼它的葉子節點必須是黑色的。

(5)從一個節點到改節點的葉子節點的所有路徑上包含相同數目的黑節點。

注意點:第(5)點就是《演算法導論》翻譯的問題,我順帶去找了一下英文版本的解釋:

Every path from a given node to any of its descendant NIL nodes contains the same number of black nodes.
就是該節點到NIL節點(也就是葉子節點)的黑色節點數目相同。

示意圖:(參考文章內此圖就是有問題的,140節點下竟讓是10和30,應該是作者配圖出錯了


針對特性(5),我們可以數一下黑色節點數目是多少,【80】節點到任意一個【nil】節點的黑色數目都是3個。

關於實踐複雜度什麼的不去關注,暫時沒想關注這些東西。省略掉這一部分;

紅黑樹操作基礎---左旋和右旋:

在紅黑樹進行新增和刪除之後,有可能當前的樹不再是一個紅黑樹了,可能特性不滿足,所以需要進行調整,而調整的時候除了顏色的更新,還有一個操作就是旋轉,旋轉包括左旋和右旋。

(1)左旋

左旋操作本質上就是將當前節點變成左節點(具體操作就是固定“當前節點X”,然後逆時針旋轉其“右孩子節點Y

”,使得Y變成X的父節點)。


演算法導論中的虛擬碼如下:(我把判斷部分單獨分開,可以看的清楚一些)

//T:紅黑樹
//nil[T]:哨兵節點,類似空節點
//每個節點存在5個域:color(顏色),key(關鍵字),left(左指標),right(右指標),p(父指標)
LEFT-ROTATE(T, x)  
 y ← right[x]              // 前提:假設x的右孩子為y。
 right[x] ← left[y]      // 將 “y的左孩子” 設為 “x的右孩子”。
 p[left[y]] ← x           // 將 “x” 設為 “y的左孩子的父親”。
 p[y] ← p[x]              // 將 “x的父親” 設為 “y的父親”

 if p[x] = nil[T]       
	then root[T] ← y       // 情況1:如果 “x的父親” 是空節點,則將y設為根節點
 else if x = left[p[x]]  
        then left[p[x]] ← y   // 情況2:如果 x是它父節點的左孩子,則將y設為“x的父節點的左孩子”
 else 
	right[p[x]] ← y         // 情況3:x是它父節點的右孩子, 將y設為“x的父節點的右孩子”

 left[y] ← x             // 將 “x”設為 “y的左孩子”
 p[x] ← y                // 將 “x ”的父節點” 設為 “y”

(2)右旋

右旋操作本質上就是將當前節點變成右節點(具體操作就是固定“當前節點Y”,然後順時針旋轉其“左孩子節點X”,使得X變成Y的父節點)。


演算法導論中的虛擬碼如下:

//T:紅黑樹
//nil[T]:哨兵節點,類似空節點
//每個節點存在5個域:color(顏色),key(關鍵字),left(左指標),right(右指標),p(父指標)
RIGHT-ROTATE(T, y)  
 x ← left[y]                // 前提:這裡假設y的左孩子為x。
 left[y] ← right[x]      // 將 “x的右孩子” 設為 “y的左孩子”。
 p[right[x]] ← y         // 將 “y” 設為 “x的右孩子的父親”。
 p[x] ← p[y]              // 將 “y的父親” 設為 “x的父親”
 
if p[y] = nil[T]       
	then root[T] ← x         // 情況1:如果 “y的父親” 是空節點,則將x設為根節點
else if y = right[p[y]]  
        then right[p[y]] ← x   // 情況2:如果 y是它父節點的右孩子,則將x設為“y的父節點的右孩子”
else 
	left[p[y]] ← x    		 // 情況3:y是它父節點的左孩子 將x設為“y的父節點的左孩子”

right[x] ← y            // 將 “y” 設為 “x的右孩子”
p[y] ← x                 // 將 “y的父節點” 設為 “x”

紅黑樹操作基礎---新增:

插入的步驟簡單來劃分可以分成3步:

(1)插入節點

因為紅黑樹其實也是二叉樹,所以可以根據樹的鍵值來確認插入的位置,然後插入節點。

(2)著色為紅色

插入之後需要將節點著色為紅色。為什麼是紅色?

因為需要保證特性(5),如果是黑色的話那麼次路徑肯定比其他路徑多了一個黑色節點,著色就不滿足要求了,所以直接著色為紅色就沒有特性(5)出現的問題。

(3)通過旋轉或者再次著色,重新滿足紅黑樹要求

5大特性,在著色之後還會出現哪些特性被影響呢?那就是特性(4),因為當前節點被著色為紅色,如果父節點也是紅色,立馬就不滿足要求。所以需要旋轉和重新著色。

新增操作虛擬碼
//待插入節點z
RB-INSERT(T, z)  
 y ← nil[T]                        // 新建節點“y”,將y設為空節點。
 x ← root[T]                       // 設“紅黑樹T”的根節點為“x”
 while x ≠ nil[T]                  // 找出要插入的節點“z”在二叉樹T中的位置“y”
     do y ← x                      
        if key[z] < key[x] 
	    then x ← left[x]  
        else 
	    x ← right[x]  
 p[z] ← y                   // 設定 “z的父親” 為 “y”
 
 if y = nil[T] 
    then root[T] ← z        // 情況1:若y是空節點,則將z設為根
 else if key[z] < key[y]        
    then left[y] ← z        // 情況2:若“z的key域” < “y的key域”,則將z設為“y的左孩子”
 else 
    right[y] ← z      	    // 情況3:“z的key域” >= “y的key域”,將z設為“y的右孩子” 
 
 left[z] ← nil[T]           // z的左孩子設為空
 right[z] ← nil[T]          // z的右孩子設為空。
 
 //第一步完成:將“節點z插入到二叉樹”中了。
 
 color[z] ← RED             // 將z著色為“紅色”
 
 //第二步完成:著色。
 
 RB-INSERT-FIXUP(T, z)      // 通過RB-INSERT-FIXUP對紅黑樹的節點進行顏色修改以及旋轉,讓樹T仍然是一顆紅黑樹

 //第三步完成:旋轉和著色。
修正操作虛擬碼
RB-INSERT-FIXUP(T, z)
while color[p[z]] = RED                                                  // 若“當前節點(z)的父節點是紅色”,則進行以下處理。
    do if p[z] = left[p[p[z]]]                                           // 若“z的父節點”是“z的祖父節點的左孩子”,則進行以下處理。
          then y ← right[p[p[z]]]                                        // 將y設定為“z的叔叔節點(z的祖父節點的右孩子)”
               if color[y] = RED                                         // Case 1條件:叔叔是紅色
				  then color[p[z]] ← BLACK                    ▹ Case 1   //  (01) 將“父節點”設為黑色。
                       color[y] ← BLACK                       ▹ Case 1   //  (02) 將“叔叔節點”設為黑色。
                       color[p[p[z]]] ← RED                   ▹ Case 1   //  (03) 將“祖父節點”設為“紅色”。
                       z ← p[p[z]]                            ▹ Case 1   //  (04) 將“祖父節點”設為“當前節點”(紅色節點)
               else if z = right[p[z]]                                   // Case 2條件:叔叔是黑色,且當前節點是右孩子
					then z ← p[z]    	                      ▹ Case 2   //  (01) 將“父節點”作為“新的當前節點”。
                               LEFT-ROTATE(T, z)              ▹ Case 2   //  (02) 以“新的當前節點”為支點進行左旋。
                          color[p[z]] ← BLACK                 ▹ Case 3   // Case 3條件:叔叔是黑色,且當前節點是左孩子。(01) 將“父節點”設為“黑色”。
                          color[p[p[z]]] ← RED                ▹ Case 3   //  (02) 將“祖父節點”設為“紅色”。
                          RIGHT-ROTATE(T, p[p[z]])            ▹ Case 3   //  (03) 以“祖父節點”為支點進行右旋。
	   else (same as then clause with "right" and "left" exchanged)      // 若“z的父節點”是“z的祖父節點的右孩子”,將上面的操作中“right”和“left”交換位置,然後依次執行。
color[root[T]] ← BLACK

修正程式碼比較複雜,場景較多,但是涉及到的場景基本都是在保證一個紅黑樹不被破壞,一般就是性質(2),(4),(5)容易被破壞,所以需要左旋,右旋等操作。

修正操作中的場景總結

虛擬碼中也列出來3個場景以及修正過程:

CaseA1:當前節點的父節點是紅色,且當前節點的父節點的另一個節點(叔叔節點)也是紅色。

處理策略---> (01)父節點設定黑色;

                    (02)叔叔節點設定黑色;

                    (03)祖父節點設定紅色。

                    (04)設定祖父節點為當前節點。

CaseA2:當前節點的父節點是紅色,叔叔節點是黑色且當前節點是父節點的右孩子。

處理策略---> (01)將父節點作為新的當前節點;(02)以當前節點為支點左旋;

CaseA3:當前節點的父節點是紅色,叔叔節點是黑色且當前節點是父節點的左孩子。

處理策略---> (01)設定父節點為黑色;(02)設定祖父節點為紅色;(03)以祖父節點為支點右旋;

當然,三種場景需要圖的結合才能更好的看出具體操作,下面列出參考文章內的圖,做了一點改正,(有些地方不對)。

需要注意虛擬碼中在進入do if的操作時需要一次判斷,else的場景下需要將left和right互換,也就是相反的操作,但是場景還是原有的場景,本系列學習就稱作CaseB1,CaseB2,CaseB3。

修正操作中的場景總結圖展示
1. 前提:

基礎紅黑樹(圖1-1);


2. 插入節點45:


45節點插入進去之後如圖2-1,插入過程非常簡單,因為本身紅黑樹就是有序了,所以只要查詢到待插入位置,然後插入,著色就行。

2.1 針對插入45之後的第一次修正(滿足CaseA1):

很顯然,這次插入違背了特性(4),父節點和子節點都是紅色了,所以進行調整。


2.2 針對插入45之後的第二次修正(依然滿足CaseA1):

第一次修正之後當前節點變成60,此時又出現了違背了特性(4),父節點和孩子節點都為紅色。因此還需要繼續修正。


2.3 針對插入45之後的第三次修正(都不滿足Case的要求,直接執行最後的Root著色操作):


至此插入動作和修正操作結束,紅黑樹重新變為合法的。但是此次插入沒有涉及到Case2和Case3的場景,需要再次分析一下這兩個場景。

這裡貼一下演算法導論裡的結論:這裡在上面給的插入完整示例內也發現了,在執行CaseA2(CaseB2)或者CaseA3(CaseB3)之後就會變成一個合法的紅黑樹。不會超過2次,超過2次就要去看看是不是場景分析錯了。


紅黑樹操作基礎---刪除:

紅黑樹的刪除分成2步:

(1)刪除節點

刪除節點和普通二叉樹一樣,需要分成3種情況:

(1.1)葉子節點:直接刪除;

(1.2)只有一個孩子:直接刪除,並將孩子節點頂替該位置;

(1.3)有兩個孩子:找出後繼節點

(2)旋轉和著色重新變為合法紅黑樹

刪除操作虛擬碼
RB-DELETE(T, z)

if left[z] = nil[T] or right[z] = nil[T]         
	then y ← z                          // 若“z的左孩子” 或 “z的右孩子”為空,則將“z”賦值給 “y”;
else 
	y ← TREE-SUCCESSOR(z)               // 否則,將“z的後繼節點”賦值給 “y”。

if left[y] ≠ nil[T]
	then x ← left[y]                    // 若“y的左孩子” 不為空,則將“y的左孩子” 賦值給 “x”;
else 
	x ← right[y]                        // 否則,“y的右孩子” 賦值給 “x”。
	
p[x] ← p[y]                             // 將“y的父節點” 設定為 “x的父節點”

if p[y] = nil[T]                               
	then root[T] ← x                    // 情況1:若“y的父節點” 為空,則設定“x” 為 “根節點”。
else if y = left[p[y]]                    
	then left[p[y]] ← x                 // 情況2:若“y是它父節點的左孩子”,則設定“x” 為 “y的父節點的左孩子”
else 
	right[p[y]] ← x                		// 情況3:若“y是它父節點的右孩子”,則設定“x” 為 “y的父節點的右孩子”

if y ≠ z                                    
   then key[z] ← key[y]                 // 若“y的值” 和“z的值不等”(也就是z有兩個孩子),則賦值給 “z”。注意:這裡只拷貝z的值給y,而沒有拷貝z的顏色!!!
   copy y's satellite data into z         

if color[y] = BLACK                            
   then RB-DELETE-FIXUP(T, x)           // 若“y為黑節點”,則呼叫

return y

基於第一步的刪除節點來簡單歸納一下它可能違反的特性:

特性(2):如果刪除y的是根,那麼y的一個紅色孩子成為了根。

特性(4):x和p[y]可能都是紅色的。也就是刪除的y是黑色,p[y]和x都是紅色的。

特性(5):刪除的y可能是黑色,導致此路徑上黑色節點少1。

和插入一樣,演算法導論裡還是在優先滿足特性(5),這樣在修正的時候就少一種不滿足,怎麼去保證的呢?

假定“x包含一個額外的黑色”,這樣就解決了特性(5)的問題,but,why?

因為x節點原本是有顏色的,要麼紅,要麼黑,當刪除的y是黑色時,x節點上移時,該路徑上就減少了一個黑色節點。所以我們假定x包含一個額外的黑色的話就解決了這個問題了,特性(5)滿足了。

但是x節點現在就會出現兩種顏色了,“紅”+“黑”或者“黑”+“黑”;這就不滿足特性(1)了,因此現在需要解決的問題就是如何去滿足特性(1),特性(2),特性(4)。

修正操作虛擬碼
RB-DELETE-FIXUP(T, x)
while x ≠ root[T] and color[x] = BLACK  
    do if x = left[p[x]]      
          then w ← right[p[x]]                                             // 若 “x”是“它父節點的左孩子”,則設定 “w”為“x的叔叔”(即x為它父節點的右孩子)                                          
               if color[w] = RED                                           // Case 1: x是“黑+黑”節點,x的兄弟節點是紅色。(此時x的父節點和x的兄弟節點的子節點都是黑節點)。
                  then color[w] ← BLACK                        ▹  Case 1   //   (01) 將x的兄弟節點設為“黑色”。
                       color[p[x]] ← RED                       ▹  Case 1   //   (02) 將x的父節點設為“紅色”。
                       LEFT-ROTATE(T, p[x])                    ▹  Case 1   //   (03) 對x的父節點進行左旋。
                       w ← right[p[x]]                         ▹  Case 1   //   (04) 左旋後,重新設定x的兄弟節點。
               if color[left[w]] = BLACK and color[right[w]] = BLACK       // Case 2: x是“黑+黑”節點,x的兄弟節點是黑色,x的兄弟節點的兩個孩子都是黑色。
                  then color[w] ← RED                          ▹  Case 2   //   (01) 將x的兄弟節點設為“紅色”。
                       x ←  p[x]                               ▹  Case 2   //   (02) 設定“x的父節點”為“新的x節點”。
                  else if color[right[w]] = BLACK                          // Case 3: x是“黑+黑”節點,x的兄弟節點是黑色;x的兄弟節點的左孩子是紅色,右孩子是黑色的。
                          then color[left[w]] ← BLACK          ▹  Case 3   //   (01) 將x兄弟節點的左孩子設為“黑色”。
                               color[w] ← RED                  ▹  Case 3   //   (02) 將x兄弟節點設為“紅色”。
                               RIGHT-ROTATE(T, w)              ▹  Case 3   //   (03) 對x的兄弟節點進行右旋。
                               w ← right[p[x]]                 ▹  Case 3   //   (04) 右旋後,重新設定x的兄弟節點。
                  else  color[w] ← color[p[x]]                 ▹  Case 4   // Case 4: x是“黑+黑”節點,x的兄弟節點是黑色;x的兄弟節點的右孩子是紅色的。(01) 將x父節點顏色 賦值給 x的兄弟節點。
                        color[p[x]] ← BLACK                    ▹  Case 4   //   (02) 將x父節點設為“黑色”。
                        color[right[w]] ← BLACK                ▹  Case 4   //   (03) 將x兄弟節點的右子節設為“黑色”。
                        LEFT-ROTATE(T, p[x])                   ▹  Case 4   //   (04) 對x的父節點進行左旋。
                        x ← root[T]                            ▹  Case 4   //   (05) 設定“x”為“根節點”。
       else (same as then clause with "right" and "left" exchanged)        // 若 “x”是“它父節點的右孩子”,將上面的操作中“right”和“left”交換位置,然後依次執行。
color[x] ← BLACK
在case4的開始時手動添加了一個else,感覺這邊缺少一個else,不然都變成case2執行之後必須要執行case4的感覺。而本質上是區分了case2,case3,case4的場景的。

核心理念:將額外的黑色沿樹上移,直到出現下面三種場景:

(1)x指向一個紅黑節點,此時在修復程式碼的最後一行直接將x單獨著色(黑色)。

(2)x指向根,直接消除額外的黑色。

(3)做必要的旋轉和顏色修改。

場景細分

(1)x是“紅”+“黑”節點;

處理方式:直接把x設為黑色,over。

(2)x是“黑”+“黑”節點,且x是根;

處理方式:什麼都不用做,over。

(3)x是“黑”+“黑”節點,且x不是根;

處理方式:需要細分具體場景進行旋轉和著色操作,最複雜。

Case1:x是“黑”+“黑”節點,x的兄弟節點w是紅色(這種情況下x的父節點和w的子節點都是黑節點)。

處理策略---> (01)w設定成黑色。

                    (02)父節點設定成紅色。

                    (03)左旋父節點。

                    (04)重新設定x的兄弟節點。


Case2:x是“黑”+“黑”節點,x的兄弟節點w是黑色,且w的兩個孩子節點也是黑色。

處理策略---> (01)w設定成紅色。

                    (02)設定x的父節點為新的x節點。

                    (03)父節點新增額外的黑色。(這個操作只是基於假定情況【一個節點具有黑+黑】下的產物)

              

此圖中的一層黑色我標註為灰色,雙層黑標註為純黑用以區分。

Case2的操作意圖分析

X節點是“黑”+“黑”,如果我們將X節點的黑顏色轉移至父節點中(如果父節點原來是紅色,那麼此時就是“紅”+“黑”,如果父節點原本是黑色,那麼此時就是“黑”+黑),但是此時,經過X節點的黑色節點數目沒有發生變化(原本X是兩個黑色,現在一個黑色跑父節點上),但是其兄弟節點w上的黑色數目多出一個(多出的一個就是父節點中的黑色)。所以需要將兄弟節點的顏色變成紅色。(此意圖比較難以理解的地方在於黑顏色上移)。

經過此操作:最後將父節點設定成當前節點,此時父節點的情況是“紅”+“黑”或者“黑”+“黑”,紅黑的話直接設定成黑色就行,黑黑的話需要進一步迴圈。

Case3:x是“黑”+“黑”節點,x的兄弟節點w是黑色,w的左孩子是紅色的,右孩子是黑色的。


Case4:x是“黑”+“黑”節點,x的兄弟節點w是黑色,w的右孩子是紅色的(主要目的是消除x節點上的多餘的一層黑色)。

處理策略---> (01)x父節點的顏色設定給兄弟節點w。

                    (02)設定x的父節點為黑色。

                    (03)設定w節點的右節點為黑色。

                    (04)對x的父節點左旋。

                    (05)設定x為根節點。

注意下圖中的修正結果不是一顆合法的紅黑樹,它只是紅黑樹的一個區域性構造。


Case4的操作意圖分析

Case4的場景中是需要去除x節點中多餘的一個黑色,Case4採用的是父節點左旋,但是左旋會帶來很多問題:

(1)兄弟節點D的左孩子在Case4中無顏色要求,因此如果左孩子是紅色的話,那麼會違反特性(4)。

基於此,需要將父節點B設定成黑色,這樣,特性(4)得以保全,此時我們來看特性(5)是不是保證的。當父節點B設定成黑色左旋之後。

(2)經過X節點(“黑”+“黑”)(也就是A節點)的黑色數目多出1來,因此此時只要將X節點上多出的一個黑去除掉就行。

(3)經過D節點左孩子(也就是C節點)的黑色數目保持一致(因為在左旋之前D是黑色的,無論C是紅是黑,只要B是黑,數目依然保持一致)。

(4)經過D節點右孩子(也就是E節點)的黑色數目不一致了,因為B和D在左旋之前交換過顏色了,D現在是紅色,做法是將E變成黑色,就解決問題了。

以上應該就是插入和刪除的全部場景了,下面貼一下刪除過程中的次數問題: