1. 程式人生 > 其它 >紅黑樹深入剖析及Java實現

紅黑樹深入剖析及Java實現

紅黑樹是平衡二叉查詢樹的一種。為了深入理解紅黑樹,我們需要從二叉查詢樹開始講起。

BST

二叉查詢樹(Binary Search Tree,簡稱BST)是一棵二叉樹,它的左子節點的值比父節點的值要小,右節點的值要比父節點的值大。它的高度決定了它的查詢效率。

在理想的情況下,二叉查詢樹增刪查改的時間複雜度為O(logN)(其中N為節點數),最壞的情況下為O(N)。當它的高度為logN+1時,我們就說二叉查詢樹是平衡的。

BST的查詢操作

T  key = a search key
Node root = point to the root of a BST

while(true){
    if(root==null){
        break;
    }
    if(root.value.equals(key)){
        return root;
    }
    else if(key.compareTo(root.value)<0){
        root = root.left;
    }
    else{
        root = root.right;
    }
}
return null;

從程式中可以看出,當BST查詢的時候,先與當前節點進行比較:

  • 如果相等的話就返回當前節點;
  • 如果少於當前節點則繼續查詢當前節點的左節點;
  • 如果大於當前節點則繼續查詢當前節點的右節點。

直到當前節點指標為空或者查詢到對應的節點,程式查詢結束。

BST的插入操作

Node node = create a new node with specify value
Node root = point the root node of a BST
Node parent = null;

//find the parent node to append the new node
while(true){
   if(root==null)break;
   parent = root;
   if(node.value.compareTo(root.value)<=0){
      root = root.left;  
   }else{
      root = root.right;
   } 
}
if(parent!=null){
   if(node.value.compareTo(parent.value)<=0){//append to left
      parent.left = node;
   }else{//append to right
      parent.right = node;
   }
}

插入操作先通過迴圈查詢到待插入的節點的父節點,和查詢父節點的邏輯一樣,都是比大小,小的往左,大的往右。找到父節點後,對比父節點,小的就插入到父節點的左節點,大就插入到父節點的右節點上。

BST的刪除操作

刪除操作的步驟如下:

  1. 查詢到要刪除的節點。
  2. 如果待刪除的節點是葉子節點,則直接刪除。
  3. 如果待刪除的節點不是葉子節點,則先找到待刪除節點的中序遍歷的後繼節點,用該後繼節點的值替換待刪除的節點的值,然後刪除後繼節點。

BST存在的問題

BST存在的主要問題是,數在插入的時候會導致樹傾斜,不同的插入順序會導致樹的高度不一樣,而樹的高度直接的影響了樹的查詢效率。理想的高度是logN,最壞的情況是所有的節點都在一條斜線上,這樣的樹的高度為N。

RBTree

基於BST存在的問題,一種新的樹——平衡二叉查詢樹(Balanced BST)產生了。平衡樹在插入和刪除的時候,會通過旋轉操作將高度保持在logN。其中兩款具有代表性的平衡樹分別為AVL樹和紅黑樹。AVL樹由於實現比較複雜,而且插入和刪除效能差,在實際環境下的應用不如紅黑樹。

紅黑樹(Red-Black Tree,以下簡稱RBTree)的實際應用非常廣泛,比如Linux核心中的完全公平排程器、高精度計時器、ext3檔案系統等等,各種語言的函式庫如Java的TreeMap和TreeSet,C++ STL的map、multimap、multiset等。

RBTree也是函式式語言中最常用的持久資料結構之一,在計算幾何中也有重要作用。值得一提的是,Java 8中HashMap的實現也因為用RBTree取代連結串列,效能有所提升。

RBTree的定義

RBTree的定義如下:

  1. 任何一個節點都有顏色,黑色或者紅色
  2. 根節點是黑色的
  3. 父子節點之間不能出現兩個連續的紅節點
  4. 任何一個節點向下遍歷到其子孫的葉子節點,所經過的黑節點個數必須相等
  5. 空節點被認為是黑色的

資料結構表示如下:

class  Node<T>{
   public  T value;
   public   Node<T> parent;
   public   boolean isRed;
   public   Node<T> left;
   public   Node<T> right;
}

RBTree在理論上還是一棵BST樹,但是它在對BST的插入和刪除操作時會維持樹的平衡,即保證樹的高度在[logN,logN+1](理論上,極端的情況下可以出現RBTree的高度達到2*logN,但實際上很難遇到)。這樣RBTree的查詢時間複雜度始終保持在O(logN)從而接近於理想的BST。RBTree的刪除和插入操作的時間複雜度也是O(logN)。RBTree的查詢操作就是BST的查詢操作。

RBTree的旋轉操作

旋轉操作(Rotate)的目的是使節點顏色符合定義,讓RBTree的高度達到平衡。 Rotate分為left-rotate(左旋)和right-rotate(右旋),區分左旋和右旋的方法是:待旋轉的節點從左邊上升到父節點就是右旋,待旋轉的節點從右邊上升到父節點就是左旋。

RBTree的查詢操作

RBTree的查詢操作和BST的查詢操作是一樣的。請參考BST的查詢操作程式碼。

RBTree的插入操作

RBTree的插入與BST的插入方式是一致的,只不過是在插入過後,可能會導致樹的不平衡,這時就需要對樹進行旋轉操作和顏色修復(在這裡簡稱插入修復),使得它符合RBTree的定義。

新插入的節點是紅色的,插入修復操作如果遇到父節點的顏色為黑則修復操作結束。也就是說,只有在父節點為紅色節點的時候是需要插入修復操作的。

插入修復操作分為以下的三種情況,而且新插入的節點的父節點都是紅色的:

  1. 叔叔節點也為紅色。
  2. 叔叔節點為空,且祖父節點、父節點和新節點處於一條斜線上。
  3. 叔叔節點為空,且祖父節點、父節點和新節點不處於一條斜線上。

插入操作-case 1

case 1的操作是將父節點和叔叔節點與祖父節點的顏色互換,這樣就符合了RBTRee的定義。即維持了高度的平衡,修復後顏色也符合RBTree定義的第三條和第四條。下圖中,操作完成後A節點變成了新的節點。如果A節點的父節點不是黑色的話,則繼續做修復操作。

插入操作-case 2

case 2的操作是將B節點進行右旋操作,並且和父節點A互換顏色。通過該修復操作RBTRee的高度和顏色都符合紅黑樹的定義。如果B和C節點都是右節點的話,只要將操作變成左旋就可以了。

插入操作-case 3

case 3的操作是將C節點進行左旋,這樣就從case 3轉換成case 2了,然後針對case 2進行操作處理就行了。case 2操作做了一個右旋操作和顏色互換來達到目的。如果樹的結構是下圖的映象結構,則只需要將對應的左旋變成右旋,右旋變成左旋即可。

插入操作的總結

插入後的修復操作是一個向root節點回溯的操作,一旦牽涉的節點都符合了紅黑樹的定義,修復操作結束。之所以會向上回溯是由於case 1操作會將父節點,叔叔節點和祖父節點進行換顏色,有可能會導致祖父節點不平衡(紅黑樹定義3)。這個時候需要對祖父節點為起點進行調節(向上回溯)。

祖父節點調節後如果還是遇到它的祖父顏色問題,操作就會繼續向上回溯,直到root節點為止,根據定義root節點永遠是黑色的。在向上的追溯的過程中,針對插入的3中情況進行調節。直到符合紅黑樹的定義為止。直到牽涉的節點都符合了紅黑樹的定義,修復操作結束。

如果上面的3中情況如果對應的操作是在右子樹上,做對應的映象操作就是了。

RBTree的刪除操作

刪除操作首先需要做的也是BST的刪除操作,刪除操作會刪除對應的節點,如果是葉子節點就直接刪除,如果是非葉子節點,會用對應的中序遍歷的後繼節點來頂替要刪除節點的位置。刪除後就需要做刪除修復操作,使的樹符合紅黑樹的定義,符合定義的紅黑樹高度是平衡的。

刪除修復操作在遇到被刪除的節點是紅色節點或者到達root節點時,修復操作完畢。

刪除修復操作是針對刪除黑色節點才有的,當黑色節點被刪除後會讓整個樹不符合RBTree的定義的第四條。需要做的處理是從兄弟節點上借調黑色的節點過來,如果兄弟節點沒有黑節點可以借調的話,就只能往上追溯,將每一級的黑節點數減去一個,使得整棵樹符合紅黑樹的定義。

刪除操作的總體思想是從兄弟節點借調黑色節點使樹保持區域性的平衡,如果區域性的平衡達到了,就看整體的樹是否是平衡的,如果不平衡就接著向上追溯調整。

刪除修復操作分為四種情況(刪除黑節點後):

  1. 待刪除的節點的兄弟節點是紅色的節點。
  2. 待刪除的節點的兄弟節點是黑色的節點,且兄弟節點的子節點都是黑色的。
  3. 待調整的節點的兄弟節點是黑色的節點,且兄弟節點的左子節點是紅色的,右節點是黑色的(兄弟節點在右邊),如果兄弟節點在左邊的話,就是兄弟節點的右子節點是紅色的,左節點是黑色的。
  4. 待調整的節點的兄弟節點是黑色的節點,且右子節點是是紅色的(兄弟節點在右邊),如果兄弟節點在左邊,則就是對應的就是左節點是紅色的。

刪除操作-case 1

由於兄弟節點是紅色節點的時候,無法借調黑節點,所以需要將兄弟節點提升到父節點,由於兄弟節點是紅色的,根據RBTree的定義,兄弟節點的子節點是黑色的,就可以從它的子節點借調了。

case 1這樣轉換之後就會變成後面的case 2,case 3,或者case 4進行處理了。上升操作需要對C做一個左旋操作,如果是映象結構的樹只需要做對應的右旋操作即可。

之所以要做case 1操作是因為兄弟節點是紅色的,無法借到一個黑節點來填補刪除的黑節點。

刪除操作-case 2

case 2的刪除操作是由於兄弟節點可以消除一個黑色節點,因為兄弟節點和兄弟節點的子節點都是黑色的,所以可以將兄弟節點變紅,這樣就可以保證樹的區域性的顏色符合定義了。這個時候需要將父節點A變成新的節點,繼續向上調整,直到整顆樹的顏色符合RBTree的定義為止。

case 2這種情況下之所以要將兄弟節點變紅,是因為如果把兄弟節點借調過來,會導致兄弟的結構不符合RBTree的定義,這樣的情況下只能是將兄弟節點也變成紅色來達到顏色的平衡。當將兄弟節點也變紅之後,達到了區域性的平衡了,但是對於祖父節點來說是不符合定義4的。這樣就需要回溯到父節點,接著進行修復操作。

刪除操作-case 3

case 3的刪除操作是一箇中間步驟,它的目的是將左邊的紅色節點借調過來,這樣就可以轉換成case 4狀態了,在case 4狀態下可以將D,E節點都階段過來,通過將兩個節點變成黑色來保證紅黑樹的整體平衡。

之所以說case-3是一箇中間狀態,是因為根據紅黑樹的定義來說,下圖並不是平衡的,他是通過case 2操作完後向上回溯出現的狀態。之所以會出現case 3和後面的case 4的情況,是因為可以通過借用侄子節點的紅色,變成黑色來符合紅黑樹定義4。

刪除操作-case 4

Case 4的操作是真正的節點借調操作,通過將兄弟節點以及兄弟節點的右節點借調過來,並將兄弟節點的右子節點變成紅色來達到借調兩個黑節點的目的,這樣的話,整棵樹還是符合RBTree的定義的。

Case 4這種情況的發生只有在待刪除的節點的兄弟節點為黑,且子節點不全部為黑,才有可能借調到兩個節點來做黑節點使用,從而保持整棵樹都符合紅黑樹的定義。

刪除操作的總結

紅黑樹的刪除操作是最複雜的操作,複雜的地方就在於當刪除了黑色節點的時候,如何從兄弟節點去借調節點,以保證樹的顏色符合定義。由於紅色的兄弟節點是沒法借調出黑節點的,這樣只能通過選擇操作讓他上升到父節點,而由於它是紅節點,所以它的子節點就是黑的,可以借調。

對於兄弟節點是黑色節點的可以分成3種情況來處理,當所以的兄弟節點的子節點都是黑色節點時,可以直接將兄弟節點變紅,這樣區域性的紅黑樹顏色是符合定義的。但是整顆樹不一定是符合紅黑樹定義的,需要往上追溯繼續調整。

對於兄弟節點的子節點為左紅右黑或者 (全部為紅,右紅左黑)這兩種情況,可以先將前面的情況通過選擇轉換為後一種情況,在後一種情況下,因為兄弟節點為黑,兄弟節點的右節點為紅,可以借調出兩個節點出來做黑節點,這樣就可以保證刪除了黑節點,整棵樹還是符合紅黑樹的定義的,因為黑色節點的個數沒有改變。

紅黑樹的刪除操作是遇到刪除的節點為紅色,或者追溯調整到了root節點,這時刪除的修復操作完畢。

RBTree的Java實現

點選閱讀原文,檢視程式碼詳情

程式碼除錯的時候,printTree輸出格式如下:

d(B)

b(B d LE) g(R d RI) a(R b LE) e(B g LE) h(B g RI) f(R e RI)

括號左邊表示元素的內容。括號內的第一個元素表示顏色,B表示black,R表示red;第二個元素表示父元素的值;第三個元素表示左右,LE表示在父元素的左邊。RI表示在父元素的右邊。

第一個元素d是root節點,由於它沒有父節點,所以括號內只有一個元素。

總結

作為平衡二叉查詢樹裡面眾多的實現之一,紅黑樹無疑是最簡潔、實現最為簡單的。紅黑樹通過引入顏色的概念,通過顏色這個約束條件的使用來保持樹的高度平衡。作為平衡二叉查詢樹,旋轉是一個必不可少的操作。通過旋轉可以降低樹的高度,在紅黑樹裡面還可以轉換顏色。

紅黑樹裡面的插入和刪除的操作比較難理解,這時要注意記住一點:操作之前紅黑樹是平衡的,顏色是符合定義的。在操作的時候就需要向兄弟節點、父節點、侄子節點借調和互換顏色,要達到這個目的,就需要不斷的進行旋轉。所以紅黑樹的插入刪除操作需要不停的旋轉,一旦借調了別的節點,刪除和插入的節點就會達到區域性的平衡(區域性符合紅黑樹的定義),但是被借調的節點就不會平衡了,這時就需要以被借調的節點為起點繼續進行調整,直到整棵樹都是平衡的。在整個修復的過程中,插入具體的分為3種情況,刪除分為4種情況。

整個紅黑樹的查詢,插入和刪除都是O(logN)的,原因就是整個紅黑樹的高度是logN,查詢從根到葉,走過的路徑是樹的高度,刪除和插入操作是從葉到根的,所以經過的路徑都是logN。

參考文獻

  1. Cormen T H, Leiserson C E, Rivest R L, 等. 演算法導論(第3版). 殷建平, 等. 機械工業出版社, 2012.
  2. Sedgewick R, Wayne K. 演算法(第4版). 謝路雲 譯. 人民郵電出版社, 2012.
  3. Weiss M A. 資料結構與演算法分析(第2版). 馮舜璽 譯. 機械工業出版社, 2004.
  4. Knuth D E. 計算機程式設計藝術 卷3:排序與查詢(英文版 第2版). 人民郵電出版社, 2010.