資料結構--紅黑樹

零星發表於2019-10-04

紅黑樹

定義

  紅黑樹也是一種平衡搜尋樹,他可以保證在最壞的情況下基本動態集合操作的時間複雜度為O(lgn)。

  紅黑樹是一棵二分搜尋樹,它在每個位置上增加了一個儲存位來表示節點的顏色,可以是RED或BLACK。通過任何一條從根到葉子的簡單路徑上各個結點的顏色進行約束,紅黑樹確保沒有一條路徑會比其他路徑常出兩倍,因而近似於平衡的。

紅黑樹與 2-3查詢樹

  紅黑樹是2-3查詢樹的一種表示方式。區別在於,2-3查詢樹中包含有2-結點3-結點。在紅黑樹中只有所有的結點都是2-結點,這樣一來就會有這樣的疑問,既然都是2-結點,那怎麼表示2-3樹呢? 答案就是,在紅黑樹中,對2-3樹中的3-結點做了一定的處理,通過使用2-結點和一些其他的資訊來表示3-結點

紅黑樹中對2-3樹中的3-結點的處理

  將2-3樹中的3-結點拆分成2個2-結點,並將這兩個2-結點的左節點使用紅色的連結,右連線使用黑色(原來2-3樹中的連結)連結,連在一起。如下圖:

資料結構--紅黑樹

這樣一來,就得到了構造紅黑樹的基本思想:

  用標準的二分搜尋樹(完全由2-結點構成)和一些額外的資訊(替換3-節點)來表示2-3樹

從上面的描述我們可以得到紅黑樹的定義:

  紅黑樹是含有紅黑連結並滿足下列條件的二分搜尋樹:

   1. 紅連結均為左連結;

   2. 沒有任何一個結點同時和兩條紅連結相連;

   3. 該樹是完美平衡的,即任意空連結到根結點的路徑上黑連結的數量相同。

紅黑樹中連線的分類:

  1. 紅連結將兩個2-結點連線起來構成一個3-結點;

  2. 黑連結則是2-3樹中的普通連結。

如下圖所示:

資料結構--紅黑樹

紅黑樹中顏色替換

  為了方便表示紅黑樹,間紅黑樹中連結的顏色表示在該連結所連線的結點中,我們都知道,一條連結的兩端,有兩個結點(父子結點),將連線的顏色儲存在子節點中。做顏色替換後,紅黑樹的示意圖如下:

資料結構--紅黑樹

上面圖片左側的部分,在連結的旁邊都有一個數字,這個數字稱為:黑高

黑高(black-height)定義

   從某個結點出發(不含該結點)到達一個葉子結點的任意一條簡單路徑上的黑色連結(結點)的個數。

   通過上述的黑高定義,及紅黑樹的平衡性可以知道紅黑樹的黑高就是根結點的黑高

紅黑樹的操作

紅黑樹的構成:

  • 節點   樹中的結點包含5個屬性:color、key、left、right、和p。

java程式碼:

public class Node{
      /** 鍵*/
      public K key;
      /** 相關聯的值*/
      public V value;
      /** 左右子樹*/
      public Node left,right;
      /** 父結點指向該結點的顏色*/
      public boolean color;
      public Node(K key, V value){
          this.key = key;
          this.value = value;
          left = null;
          right = null;
          color = RED;
      }
  }
複製程式碼
紅黑樹圖示:
資料結構--紅黑樹

紅黑樹的性質

  • 1.每個節點或是紅色的,或是黑色的;

  • 2.根結點是黑色的;

  • 3.每個葉子結點(空連結)是黑色的;

  • 4.如果一個結點是紅色的,則它的兩個葉子結點都是黑色的;

  • 5.對每一結點,從該結點到其所有後代葉結點的簡單路徑上,均包含有相同數目的黑色結點。

旋轉

  在對紅黑樹進行操作(插入或刪除)的時候,可能會出現右連結為紅色連結,或者有兩條連續的紅色連結。這樣一來就破壞紅黑樹的性質。因此需要 在本次操作完成之前通過旋轉使得本次操作後,該樹仍然是紅黑樹。旋轉操作會改變紅連結的指向。旋轉也分為左旋轉右旋轉

  左旋轉:將紅色連線為右連結轉化為左連結。圖示如下:

資料結構--紅黑樹

總結:

  上述圖片中,結點值為8的結點顏色可以是紅色,夜可以是黑色。該結點同樣也可是左子樹,也可以是右子樹。上述左旋轉的過程:將上述圖片的部分看成一棵子樹,該子樹的根結點的值是8。經過旋轉後將值為15的結點作為該樹的根結點。(將兩個鍵中值較小的左為跟結點變成值交大者作為根結點的過程)。

  • 左旋轉的程式碼實現:
private Node leftRotate(Node node){
    Node x = node.right;
    //左旋轉
    node.right = x.left;
    x.left = node;

    x.color = node.color;
    node.color = RED;
    return x;
}
複製程式碼

  右旋轉:將紅色連線為左連結轉化為右連結。圖示如下:

資料結構--紅黑樹

總結:

  紅黑樹右旋轉的過程就是左旋轉的逆向過程。

  • 右旋轉的程式碼實現:
private Node rightRotate(Node node){
    Node x = node.left;
    node.left = x.right;
    x.right = node;

    x.color = node.color;
    node.color = RED;
    return x;
}
複製程式碼

插入

  為了更好的理解紅黑樹,在上一篇文章中首先學習了2-3樹,這一篇文章中關於紅黑樹的學習,也是通過2-3樹經過相應的變化而來。這裡要對紅黑樹 進行插入結點的操作,同樣類比2-3樹的插入。來看看,在紅黑樹的插入過程中,是如何維護紅黑樹的性質的。

  在紅黑樹的插入操作中,假設插入的節點都為紅色。

這裡假設結點為紅色,插入紅黑樹之後,會破壞上述的性質4。通過上述的旋轉過程可以進行相應的調整來維護紅黑樹。這裡如果假設插入的是黑色的結點,就會破壞紅黑樹的平衡性(性質5)。

向2-結點中插入新鍵

  向2-結點中插入新鍵分為如下的兩種情況。

紅黑樹根結點的左側插入

  在紅黑樹的根結點的左側插入,預設插入是紅結點,而紅黑樹根結點為黑節點,插入該結點後,紅黑樹的性質不變。

資料結構--紅黑樹
紅黑樹根結點的右側插入

  在紅黑樹的根結點的右側插入,由於預設插入的是紅色結點,插入後不滿足紅黑樹的性質,此時右節點為紅色結點。通過左旋轉,將其旋轉為左結點為紅色結點,修正根結點的連線。

資料結構--紅黑樹

總結:

  經過上述操作後,該紅黑樹等價為一棵只有一個3-結點的2-3樹,該紅黑樹有兩個結點,其中一個為紅色結點,樹的黑高為1。

向樹底部的2-結點中插入新鍵

  用和二分搜尋樹相同的方式向一棵紅黑樹中插入一個新鍵會在樹的底部新增一個結點(保證樹的有序性),同樣也是紅結點和其父結點相連。如果父結點是一個2-結點,那麼上述的兩種處理方式仍然適用。如果指向新結點的是父結點的左連結,那麼父結點就直接成為一個3-結點;如果指向新結點的是父結點的右連結,這就是一個錯誤的3-結點,通過一次左旋轉來修正。

資料結構--紅黑樹

向一棵雙鍵樹(3-結點)中插入新鍵

  雙鍵樹中插入新的鍵分為如下的三種情況,下面分類討論:

新插入的鍵大於原樹中的兩個鍵

  要插入的鍵在大於原樹中的兩個鍵,此時根結點連結兩個紅結點,不符合紅黑樹的性質,只需將兩個紅色節點變為黑色節點即可。

資料結構--紅黑樹
新插入的鍵介於原樹中的兩個鍵之間

  新插入的鍵介於原樹兩個鍵之間,這會產生兩個連續的紅色結點,一個結點是左結點,一個結點是右結點。需要將紅色的右結點先左旋轉變,使其變成兩個連續的紅色結點;然後在進行右旋轉使其變成一個結點連結兩個紅色的結點(一左一右);最後在對兩個紅色結點的顏色進行變換。

資料結構--紅黑樹
新插入的鍵小於原樹中的兩個鍵

  新插入的鍵小於原樹中的兩個鍵,新插入的結點會被連結到最左邊的空連結,這樣也產生了兩個連續的紅色結點,將從根結點開始的第一個紅結點右旋轉,使根結點連結兩個紅色的結點;然後,將兩個紅色結點的顏色變為黑色。

資料結構--紅黑樹
顏色轉換

  當一個結點連結兩個紅色的子結點時,需要將子結點的顏色右紅變黑,同時也需要將父結點的顏色由黑變紅。

程式碼實現

private void flipColors(Node node){
    node.color = RED;
    node.left.color = BLACK;
    node.right.color = BLACK;
}
複製程式碼
根結點總是黑色的

  在發生顏色轉換的時候,會遇到根結點連結連個紅色結點的情況。此時進行顏色轉換後根結點為紅色,當紅色及結點出現在根結點的時候,紅黑樹的黑高就會增加1。上篇文章中有說到,2-3樹的節點與紅黑樹結點的對應關係。而紅色結點的來源就是3-結點拆成兩個2結點,然後將左2-結點標記為紅色。在每次新增新的結點後,都將紅黑樹的根結點設定為黑色。

向樹底部的3-結點中插入新鍵

  假設在樹的底部的一個**3-**結點下加入一個新的結點。上面討論的三種情況都會出現。指向新結點的連結可能是3-結點的右連結(此時需要轉換顏色),或是左連結(需要右旋轉然後再轉換顏色),或是中連結(需要先左旋轉下層連結然後右旋轉上層連結,最後再轉換顏色)。顏色轉換會使到中結點的連結變紅,相當於將它送入了父結點。這意味著在父結點中繼續插入一個新鍵,使用相同的辦法解決這個問題。

將紅連結在樹中向上傳遞

  2-3樹中插入演算法需要我們分解3-結點,將中間鍵插入父結點,如此這般直遇到一個2-結點或根結點。之前考慮過的所有情況都是為了達成這個目標:每次必要的旋轉之後我們都會進行顏色轉換 使得根結點變紅。站在父結點的角度來看,處理這樣的一個紅色結點的方式和處理一個新插入的紅色結點完全相同,繼續把紅連結轉移到中間結點上去。下圖中總結的三種情況顯示了在紅黑樹實現2-3樹的插入演算法的關鍵操作所需步驟:要在一個3-結點下插入新鍵,先建立一個臨時的4-結點,將其分解並將紅連結由中間鍵傳遞給它的父結點。重複這個過程,我們就能將紅連結在樹中向上傳遞,直至遇到一個2-結點或根結點。

資料結構--紅黑樹

總結:

  只要謹慎的使用左旋轉,右旋轉和顏色轉換這三個簡單的操作,就可以保證插入操作後紅黑樹和2-3樹的一一對應關係。在沿著插入點到根結點的路徑向上移動時在所經過的每個結點中順序完成以下操作,即可完成插入操作

  • 如果右子結點是紅色的而左子結點是黑色的,進行左旋轉;

  • 如果左子結點是紅色的且它的左子結點也是紅色的,進行有旋轉;

  • 如果左右子結點均為紅色,進行顏色轉換。

紅黑樹的插入程式碼實現

public void add(K key, V value){
     root = add(root, key, value);
     root.color = BLACK;
 }
複製程式碼
private Node add(Node node,K key, V value){
    if(node == null){
        size++;
        //預設插入紅色節點
        return new Node(key,value);
    }
    if(key.compareTo(node.key) < 0){
        node.left = add(node.left,key,value);
    }else if(key.compareTo(node.key) > 0){
        node.right = add(node.right,key,value);
    }else{
        node.value = value;
    }
    if(isRed(node.right) && !isRed(node.left)){
        node = leftRotate(node);
    }
    if(isRed(node.left) && isRed(node.left.left)){
        node = rightRotate(node);
    }
    if(isRed(node.left) && isRed(node.right)){
        flipColors(node);
    }
    return node;
}
複製程式碼

  上述程式碼中的isRed()方法用來判斷結點的顏色。

private boolean isRed(Node node){
    if(node == null){
        return BLACK;
    }
    return node.color;
}
複製程式碼

個人微信公眾號:

資料結構--紅黑樹
個人github:

github.com/FunCheney

相關文章