1. 程式人生 > 實用技巧 >平衡查詢樹(2-3樹和紅黑樹)

平衡查詢樹(2-3樹和紅黑樹)

在一般情況下,二叉樹的查詢效率很高,但在極個別情況下會出問題,這依賴於輸入資料的順序。

如果我們給BTree<Char,Int>(代表鍵是字元值是整數的二叉樹)插入這樣一串資料:[1,2,3,4,5],那我們得到的二叉樹就是這樣的。

二叉樹退化成了連結串列,而我們查詢一個數據的時間複雜度也退化成了\(O(n)\)

平衡查詢樹用於解決這個問題,平衡查詢樹中會做一些操作,使得插入的資料在樹中分佈的很平衡,樹的高度以對數級別上升,這樣的話查詢起來就非常快了。如上圖的資料在平衡查詢樹裡可能會被插入成這樣

2-3樹

2-3樹通過把節點分成2節點和3節點,並通過向上分裂的操作保證搜尋樹的平衡性。

2節點

2節點指有左右兩個子節點的節點。同時此節點中有一個數據元素。和二叉搜尋樹中的節點一樣,左子節點中的資料比當前節點的資料小。右子節點中的資料比當前節點的資料大。

3節點

3節點指有左中右三個子節點的節點。同時節點中存在兩個資料元素。左側元素比右側元素小,左側子節點中的元素比左側元素小,中間子節點的元素大於左側子節點的元素小於右側子節點的元素,右側子節點中的元素大於右側元素。

示例

插入

如果想造出一顆平衡搜尋樹,肯定要在插入和刪除的時機做一些手腳,才能保證樹中的元素分佈平衡。

對於2-3樹,我們需要考慮如下兩種情況:

  1. 要插入的節點位置的父節點是2節點
  2. 要插入的節點位置的父節點是3節點

對於第一個情況,我們又可以細分為兩種情況

  1. 待插入的節點比父節點小
  2. 待插入的節點比父節點大

因為我們要保持整棵樹的平衡性,所以我們不能直接像二叉查詢樹一樣,把節點直接掛在2節點下,這有時會增加樹的高度。

如果我們不想改變樹的高度,那麼我們就可以把待插入節點插入到父節點當中去,使父節點變為一個3節點,但3節點有元素大小的順序規定,所以我們不能隨便插入。分如下兩種情況:

當第二種情況發生的時候,也就是父節點是一個3節點,那就稍微有些麻煩,我們不能把待插節點簡單的加到父結點中,因為這會變成一個4節點,2-3樹中沒有四節點。這時候我們可以採取向上分裂的辦法。當然這又得分三種情況。

  1. 待插節點小於3節點左側元素
  2. 待插節點大於3節點左側元素小於3節點右側元素
  3. 待插節點大於3節點右側元素

如上圖,我們把一個4節點(3節點加1待插節點)按三種情況分裂成了三個2節點。但是可以看到樹的高度增加了,按理說是破壞了平衡性。其實不是,我們用遞迴的方式,把4節點中提出去的那個根節點(Case1中的H,Case2中的J,Case3中的L)當作新的待插入節點,再去判斷它的父節點,如果它的父節點是2節點,就按照上面的規則把它歸併到父節點中成為一個3節點,如果父節點是3節點就繼續分裂,一直到樹根。

到了樹根後,樹的整體高度+1。

區域性變化

講了這麼多,主要的還是要理解,2-3查詢樹的所有插入操作都是區域性操作,歸併和分裂操作使得插入一個節點不影響整體的平衡性。

總結

2-3搜尋樹是由下向上生長的,相比於二叉搜尋樹,2-3樹能在任何情況下都保證最壞的查詢情況在對數級別。當然,這要從插入和刪除操作的時間複雜度和實現複雜性上來做犧牲。

2-3查詢樹確實是一個不錯的想法,但是實現起來非常不方便,要處理的情況非常多,寫出來程式碼也會很醜。所以下面來看一個基於二叉樹的2-3樹等價替代品——紅黑樹。

紅黑樹

紅黑樹通過設定連結顏色來模擬2-3樹。推薦把紅黑樹的所有操作與2-3樹的操作做對比,你會發現實際上就是一個等價代換。

紅黑樹用紅連結代表3節點結構,用黑連結代表正常父子關係。下面是一個2-3樹和紅黑樹的等價代換。

如果把紅連結橫過來,就更形象了。

為了讓邏輯清淅,規定紅黑樹有如下性質:

  1. 紅連結只能是指向左子節點的連結
  2. 沒有任何一個節點同時和兩條紅連結相連
  3. 完美黑色平衡,所有空連結到根節點的距離相同

旋轉

使用旋轉操作保證上面的三條性質。

規定兩個操作,分別是左旋和右旋。先看左旋,很形象。

虛擬碼

//h就是上圖中的E節點
Node rotateLeft(Node h){
    Node x = h.right;
    h.right = x.left;
    x.left = h;
    //!!!!!!color描述的是父節點到該節點的顏色!!!!!!
    x.color = h.color;
    h.color = RED;
    return x;
}

右旋操作則相反

虛擬碼

Node rotateRight(Node h){
    Node x = h.left;
    h.left = x.right;
    x.right = h;
    x.color = h.color;
    h.color = RED;
    return x;
}

插入

左旋右旋的操作看起來迷迷糊糊的不知道是在幹啥,但是把它應用到插入演算法中,立馬就知道它的功效了。

在這之前還要說一嘴,雖然上面的程式碼裡已經說過了。h.color代表的是h的父節點到h之間的連結的顏色。我們說h是紅的,意思是它到它的父節點之間連結是紅色的。

插入演算法也有兩種情況

  1. 待插入節點的父節點顏色是黑的
  2. 待插入節點的父節點顏色是紅的

和2-3樹比較著看,這兩條其實和2-3樹的情況完全相同,第一個代表父節點是個2節點,第二個代表父節點是個3節點。

對於父節點是黑節點(2節點)的插入,如果待插節點比父節點小,則直接插入到左子節點並設定成紅色,類比2-3樹就是把待插節點和父節點合併成3節點並且把待插節點放到3節點的左側。如果待插節點比父節點大,則也是直接插入到右節點,同時也設定成紅色,但是紅黑樹不允許右紅節點,所以進行一次左旋操作就好了。

對於父節點是紅節點(3節點)的插入,我們又要分成三種情況:

  1. 待插入節點比父節點中的兩個鍵小
  2. 待插入節點介於父節點的兩個鍵之間
  3. 待插入節點比父節點的兩個鍵大

和2-3樹一樣,這裡需要一個向上遞迴的分裂操作。

如果待插入節點比父節點中的兩個鍵小,直接插入到父節點的左子節點上,設定為紅色節點,並右旋它的父節點。介於兩者之間的情況則可以把新鍵插入到父節點的中子節點上(等價完2-3樹後的中子節點,紅黑樹中沒有中子節點),並設定為紅色節點,然後左旋它。待插入節點最大的情況下,只需要把它接到父節點的最右子結點上,並設定為紅色。

大概就是這樣的。

最後的三種狀態是一樣的,左右紅連結代表了三個元素構成了一個4節點,在2-3樹中,這種情況要進行向上分裂,分裂成三個2節點,再把作為父節點的2節點向上遞迴操作。

紅黑樹中也是這樣的,而且相同的操作在紅黑樹中更簡單,只需要把A和C變成黑節點就行了。

刪除節點

未完...

C++實現

未完...