2-3樹 與 紅黑樹

前言

紅黑樹直接看有點懵,塗上顏色,顏色轉換,狀態調整,說實話,一上來這麼弄,我都不想看,有些東西你會知道是這麼回事,但是你不清楚爲什麼這麼做?爲什麼要塗上顏色?我自己也不太喜歡死記硬背,感覺很傷腦子,而且過一段時間就忘記了,這不是我的風格。

2-3 樹

2-3 樹 同2-3-4樹 是差不多的概念,這也是一種平衡樹,但有不一樣的地方:

  • 一般平衡樹一個節點只能存一個key,這種樹的節點可以有兩個key,有兩個key的節點稱爲3節點,有一個key的節點稱爲2節點,這裏不是說 2個節點,就叫2節點。下圖左邊是2節點,右邊是3節點。
    2節點3節點
  • 2節點只有一個左子樹、一個右子樹,3節點有三個子樹,分別是左子樹、中間子樹、一個右子樹。
  • 平衡樹都有 節點旋轉操作,因爲要保證左子樹、右子樹 高度不能相差1,2-3樹也有,但是它的旋轉會複雜一些

同樣 2-3樹的插入、刪除也會更復雜,大致的思路是:

  • 如果是2節點,就變成3結點
  • 如果是3結點,3結點成4節點,4節點重新分配節點

刪除的話就複雜很多,當然被篇文章就不仔細討論這些了,他的優點包括平衡二叉樹的所有優點:

  • 查找時間複雜度:log(n)log(n),最壞的情況下降低了樹的高度,比二叉平衡樹可以容納更多的key

當然缺點也非常致命:

  • 查找和插入操作的實現需要大量的代碼,而且它們所產生的額外開銷可能會使算法比標準的二叉查找樹更慢

所以2-3樹在實際中很少使用。由於其需要大量的節點變換(從2-節點到3-節點到4-節點甚至到5-節點…),這些變換在實際代碼中是很複雜的。所以現在幾乎沒有2-3樹的具體實現。
但是由於2-3樹的變化十分直觀,因此前人在2-3樹的理論基礎上發明了紅黑樹。

2-3-4樹與紅黑樹

2-3-4樹 與 2-3樹唯一的差別就在於多了一個4結點,容納3個key,其他差別不太大,紅黑樹其實更類似2-3-4樹:

  • 一個黑結點 如果有 兩個紅結點,就類似一個4結點
  • 一個黑結點如果有一個紅結點,就類似一個3結點
    2-3樹 和 2-3-4樹 雖然很方便,但是上一節中也提到代碼會非常複雜,於是改善了一下,提出了紅黑樹,紅黑樹用紅鏈接表示2-3樹中另類的3-結點,統一了樹中的結點類型,使代碼實現簡單化,又不破壞其高效性,用顏色來做一個標識。

有一些博客裏講解了紅黑樹的幾個特徵性質,當然我們也提一下,雖然這些特徵比較麻煩:

  • 1、根結點是黑色
  • 2、沒有相鄰的紅結點,意思就是父結點是紅結點,子結點就不能是紅結點,紅結點的子結點必須是黑結點
  • 3、從結點(包括根)到其任何後代NULL節點(葉子結點下方掛的兩個空節點,並且認爲他們是黑色的)的每條路徑都具有相同數量的黑色節點

這是約束一顆紅黑樹的條件,也就是符合這些條件就是紅黑樹,其實還有一點:新插入的結點都是紅色節點。紅色節點插入樹中之後,根據約束條件來更改顏色、調整樹結構。

紅黑樹插入結點

插入相對還是比較簡單的。同樣結點數目構成的紅黑樹有很多種,這些不同的情況都可以通過染色、平衡來實現,那麼問題來了?你依據什麼樣的準則來染色、調整?因爲很多種染色、平衡操作都可以使得不平衡的紅黑樹變得平衡,那麼你設計的調整準則是什麼?

看到一篇文章講解插入操作,講解的還是比較好的,插入主要關注三種情況,這三種情況會因爲一開始插入,或者是因爲調整之後出現的情況,並且並不是調整一次就結束,會反覆調整,我們先看下僞代碼:


RB-INSERT-FIXUP(T,z)
 1  while color[p[z]] = RED  
 2      if p[z] = left[p[p[z]]]  
 3                y ← right[p[p[z]]]  
 4                if color[y] = RED  
 5                   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                           z ← p[z]                       ▹ Case 2  
11                           LEFT-ROTATE(T, z)              ▹ Case 2  
12                 else   
13                     color[p[z]] ← BLACK                 ▹ Case 3  
14                     color[p[p[z]]] ← RED                ▹ Case 3  
15                     RIGHT-ROTATE(T, p[p[z]])            ▹ Case 3  
16      else (same as then clause   with "right" and "left" exchanged)  
17  color[root[T]]  ←  BLACK  

代碼可能有點難懂,參數說明:z是插入節點的指針,T是樹。總結一下代碼流程:

while  ( z節點父節點 是紅色 ):
         if  z節點的父節點  ==  z節點 祖父節點的左節點:
                y = z節點 祖父節點右節點
                if y 顏色 == red
                       第一種情況調整                                     ▹ 注意
                       z = z節點的祖父節點
                 else if z是右孩子    #  這也說明 y 顏色是黑色
                           z = z節點的父節點
                           第二種情況調整                                 ▹ 注意
                 else  # z是左孩子 ,y 顏色是黑色
                        第三種情況調整                                    ▹ 注意
         else # 隱藏含義: z節點的父節點  ==  z節點 祖父節點的右節點
              # 剩下的操作跟上面一樣,只不過 左右互換一下

最後將 根節點 顏色 改爲 黑色

注意幾點:

  • 調整一共有三種情況
  • 這是一個while 循環,三種情況 反覆調整,一直都 z節點 的父節點不是紅色
  • 最後一步:將根節點置位黑色

那麼調整的三種情況是怎麼樣的呢?圖解如下:

講解:

  • 紅色節點是插入節點:這句話是說 紅色圈起來的節點,節點的圓圈是紅色筆畫的,
  • 調整後,zz被我用紅色字體標出來了,這就是下一個循環中用的zz節點。
  • 第一種情況:變色
  • 第二種情況:左旋轉
  • 第三種情況:父節點變黑,祖父節點變紅,變色後 右旋轉

看的時候注意幾個問題:

  • 第一種情況下:爲什麼根節點是紅色?這不就不符合紅黑樹定義了嘛?
    因爲有最後的一個步驟將根節點置位黑色,所以其實也不算是一種失誤

  • 第二種情況、第三種情況 下 原有結構也是有問題的?
    因爲我們看到了的只是一部分結構,單純的這種樹結構是不會出現的,因爲根本不符合紅黑樹的定義,建議看下教你透徹瞭解紅黑樹。裏面有完整的樹結構,第二種、第三種其中一部分結構,並且也有可能是調整之後出現的情況

  • 第二種情況、第三種情況的聯繫
    如果你仔細看,你會發現第二種情況跟第三種情況是連續的,第二種情況之後就開始執行第三種情況,

  • 第三種情況之後好像出現了不平衡的情況
    第三種情況是先變色,再旋轉,但其實第三種情況的初始狀態就不會存在,也就是不存在單純的第三種情況的初始狀態,它只是紅黑樹的一部分。完整的樹下它是平衡的。

  • 出現連續的兩個紅色節點的情況,比如第三種情況,有點像2-3-4樹
    紅-紅-黑 就像 一個 4結點,只不過在2-3-4樹中是一個結點,在紅黑樹中要變色、調整,使得平衡。

  • 第三種情況之後,似乎就結束調整了
    沒錯,我們看圖中第三種情況調整之後,zz的節點位置,發現它不滿足while循環的條件,也就會跳出循環,所以可以認爲這是最後的一個步驟。

  • 爲什麼是這三種情況?
    很奇怪的一點就是爲什麼是這三種情況?這三種情況似乎是可以連續起來的,2、3、1這樣的順序,不過也只是猜測,各種情況分析之後其實也就只有這幾種情況需要調整了,具體的還要繼續研究一下。

插入的操作大致是這樣,一些不需要調整的情況如下:

  • 插入的是根結點,因爲原樹是空樹,此情況只會違反性質2,所以直接把此結點塗爲黑色。
  • 如果插入的結點的父結點是黑色,由於此不會違反性質2和性質4,紅黑樹沒有被破壞,所以此時也是什麼也不做。

三種情況的另一種換言之,可以加強大家認識:

  • 情況1:如果當前結點的父結點是紅色且祖父結點的另一個子結點(叔叔結點)是紅色
  • 情況2:當前結點的父結點是紅色,叔叔結點是黑色,當前結點是其父結點的右子
  • 情況3:當前結點的父結點是紅色,叔叔結點是黑色,當前結點是其父結點的左子

左右反過來的情況是一樣的,處理過程也是類似的,這裏不贅述。

		TreeSet treeSet = new TreeSet();
        treeSet.add(4);
        treeSet.add(1);
        treeSet.add(2);
        treeSet.add(3);
        treeSet.add(5);
        treeSet.add(6);
        treeSet.add(7);

以上代碼是我用來測試的代碼,通過debug來查看了每個節點的顏色,得到了最終樹的形狀:

我比較奇怪,爲啥 這個不是其他結構,因爲4也可以作爲根節點,得到一個更加平衡的二叉樹,於是我猜想是不是跟節點插入順序有關,於是我重新弄了一個代碼:

       TreeSet treeSet = new TreeSet();
        treeSet.add(4);
        treeSet.add(1);
        treeSet.add(5);
        treeSet.add(2);
        treeSet.add(6);
        treeSet.add(3);
        treeSet.add(7);

我debug看了一下,還是結構發生了變化,

插入的順序跟最終的結果關係很大,有可能本來就很平衡,不需要調整。

紅黑樹 刪除結點

刪除結點 一共有6種情況,真是複雜,慢慢來分析,這裏會先講解刪除樹節點的大致操作,然後來了解一下二叉樹刪除節點之後的調整過程,定一個基調,用來分析紅黑樹的刪除:

  • 1、葉子節點刪除,直接刪除
  • 2、只有一個子節點的節點,刪除該節點,子節點替代上去
  • 3、有兩個子節點的節點刪除,選擇左子節點的最大節點來替代,默認這麼做,也可以選擇右邊最小子節點。
    不過紅黑樹是先刪除再調整樹結構,調整樹結構時會以刪除節點的位置出發來做調整操作。
    所以如果是紅黑樹的話,應該怎麼做呢?
  • 當前結點是紅節點
  • 刪除後,替代節點如果是黑色節點,且是根節點,則直接刪除

這兩種情況比較好弄,但是不太好理解,第一種比較好理解,第二種不太好理解,因爲極有可能會造成左子樹黑色結點少了一個,這樣,根節點到葉子節點的路徑上黑色節點數目不太一致,這一點先保留意見,之後來測試一下場景。
還有另外四種情況,代碼先上:

 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  

4種情況主要有:

  • Case 1:當前結點是黑色,且兄弟結點爲紅色(此時父結點和兄弟結點的子結點分爲黑)
    下圖中A節點就是當前節點,把父結點染成紅色,把兄弟結點染成黑色,之後重新進入算法。此變換前、後 原紅黑樹性質5不變,而把問題轉化爲兄弟結點爲黑色的情況。
  • Case 2:當前結點是黑色,且兄弟是黑色且兄弟結點的兩個子結點全爲黑色
    這裏要注意,調整後會將B節點作爲當前節點,然後進行插入調整。所以下面的圖可以看出其實是不平衡的,刪除調整之後還需要插入調整
  • Case 3:當前結點顏色是黑,兄弟結點是黑色,兄弟的左子是紅色,右子是黑色
    把兄弟結點染紅,兄弟左子結點染黑,之後再在兄弟結點爲支點解右旋,之後重新進入算法,這主要是把當前情況轉化爲情況4,然後通過插入調整來使得滿足性質:根節點到所有葉子節點路徑中黑色節點數目一致。這裏有個疑問?插入調整的當前節點是哪個?
  • Case 4:當前結點顏色是黑,它的兄弟結點是黑色,但是兄弟結點的右子是紅色,兄弟結點左子的顏色任意
    把兄弟結點染成當前結點父結點的顏色,把當前結點父結點染成黑色,兄弟結點右子染成黑色,之後以當前結點的父結點爲支點進行左旋,然後進行插入調整

其實通過上述的情況來看,這裏的刪除調整不是一下子就結束了,也要進行插入調整,兩個調整之後還要進行多次調整,最終可以得到一顆符合條件的平衡樹。
來看一個代碼實現:

        TreeSet treeSet = new TreeSet();
        treeSet.add(4);
        treeSet.add(1);
        treeSet.add(5);
        treeSet.add(2);
        treeSet.add(6);
        treeSet.add(3);
        treeSet.add(7);


        treeSet.remove(2);

通過debug發現刪除前後的差別是,刪除前:

刪除後:

紅黑樹 業務總結

目前樹結構的有:

  • 二叉查找樹(BST)
    英文Binary Sort Tree,查找、插入、刪除的時間複雜度爲O(logN)O(logN),時間複雜度就變味了O(N)O(N)
  • AVL 樹
    平衡二叉樹全稱平衡二叉搜索樹,也叫AVL樹。是一種自平衡的樹。它要求左子樹、右子樹的高度相差不超過1,如果超過1了就會執行調整算法來調整數結構。

紅黑樹也是一種AVL樹,紅黑樹是用非嚴格的平衡來換取增刪節點時候旋轉次數的降低,任何不平衡都會在三次旋轉之內解決,而AVL是嚴格平衡樹,因此在增加或者刪除節點的時候,根據不同情況,旋轉的次數比紅黑樹要多。

實際應用中,根據業務特點來選擇:

  • 若搜索的次數遠遠大於插入和刪除,那麼選擇AVL
  • 如果搜索,插入刪除次數幾乎差不多,應該選擇RB

參考博客

漫畫算法:什麼是紅黑樹?(通俗易懂)
2-3樹到紅黑樹
紅黑樹從頭至尾插入和刪除結點的全程演示圖
RedBlack.pdf

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章