1. 程式人生 > >史上最簡單清晰的紅黑樹講解

史上最簡單清晰的紅黑樹講解

原文來自http://blog.csdn.net/yang_yulei/article/details/26066409

查詢(一)

我們使用符號表這個詞來描述一張抽象的表格,我們會將資訊()儲存在其中,然後按照指定的來搜尋並獲取這些資訊。鍵和值的具體意義取決於不同的應用。

符號表中可能會儲存很多鍵和很多資訊,因此實現一張高效的符號表也是一項很有挑戰性的任務。

我們會用三種經典的資料型別來實現高效的符號表:二叉查詢數紅黑樹散列表

二分查詢

我們使用有序陣列儲存鍵,經典的二分查詢能夠根據陣列的索引大大減少每次查詢所需的比較次數。

在查詢時,我們先將被查詢的鍵和子陣列的中間鍵比較。如果被查詢的鍵小於中間鍵,我們就在左子陣列中繼續查詢,如果大於我們就在右子陣列中繼續查詢,否則中間鍵就是我們要找的鍵。

一般情況下二分查詢都比順序查詢快的多,它也是眾多實際應用程式的最佳選擇。對於一個靜態表(不允許插入)來說,將其在初始化時就排序是值得的。

當然,二分查詢也不適合很多應用。現代應用需要同時能夠支援高效的查詢和插入兩種操作的符號表實現。也就是說,我們需要在構造龐大的符號表的同時能夠任意插入(也許還有刪除)鍵值對,同時也要能夠完成查詢操作

要支援高效的插入操作,我們似乎需要一種鏈式結構。當單鏈接的連結串列是無法使用二分查詢的,因為二分查詢的高效來自於能夠快速通過索引取得任何子陣列的中間元素。為了將二分查詢的效率和連結串列的靈活性結合起來,我們需要更加複雜的資料結構。

能夠同時擁有兩者的就是二叉查詢樹

二叉查詢樹

一顆二叉查詢樹(BST)是一顆二叉樹,其中每個節點都含有一個可比較的鍵(以及相關聯的值)且每個結點的鍵都大於其左子樹中的任意結點的鍵而小於右子樹的任意結點的鍵

 

一顆二叉查詢樹代表了一組鍵(及其相應的值)的集合,而同一個集合可以用多顆不同的二叉查詢樹表示。

如果我們將一顆二叉查詢樹的所有鍵投影到一條直線上,保證一個結點的左子樹中的鍵出現在它的右邊,右子樹中的鍵出現在它的右邊,那麼我們一定可以得到一條有序的鍵列。


查詢

在二叉查詢樹中查詢一個鍵的遞迴演算法:

如果樹是空的,則查詢未命中。如果被查詢的鍵和根結點的鍵相等,查詢命中。否則我們就在適當的子樹中繼續查詢。如果被查詢的鍵較小就選擇左子樹,較大就選擇右子樹。

在二叉查詢樹中,隨著我們不斷向下查詢,當前結點所表示的子樹的大小也在減小(理想情況下是減半)

插入

查詢程式碼幾乎和二分查詢的一樣簡單,這種簡潔性是二叉查詢樹的重要特性之一。而二叉查詢樹的另一個更重要的特性就是插入的實現難度和查詢差不多

當查詢一個不存在於樹中的結點並結束於一條空連結時,我們需要做的就是將連結指向一個含有被查詢的鍵的新結點。如果被查詢的鍵小於根結點的鍵,我們會繼續在左子樹中插入該鍵,否則在右子樹中插入該鍵。

分析

使用二叉查詢樹的演算法的執行時間取決於樹的形狀,而樹的形狀又取決於鍵被插入的先後順序。

在最好的情況下,一顆含有N個結點的樹是完全平衡的,每條空連結和根結點的距離都為~lgN。在最壞的情況下,搜尋路徑上可能有N個結點。但在一般情況下樹的形狀和最好情況更接近。


我們假設鍵的插入順序是隨機的。對這個模型的分析而言,二叉查詢樹和快速排序幾乎就是“雙胞胎”。樹的根結點就是快速排序中的第一個切分元素(左側的鍵都比它小,右側的鍵都比它大),而這對於所有的子樹同樣適用,這和快速排序中對於子陣列的遞迴排序完全對應。

【在由N個隨機鍵構造的二叉查詢樹中,查詢命中平均所需的比較次數為~2lgN。 N越大這個公式越準確】

平衡查詢樹

在一顆含有N個結點的樹中,我們希望樹高為~lgN,這樣我們就能保證所有查詢都能在~lgN此比較內結束,就和二分查詢一樣。不幸的是,在動態插入中保證樹的完美平衡的代價太高了。我們放鬆對完美平衡的要求,使符號表API中所有操作均能夠在對數時間內完成。

2-3查詢樹

為了保證查詢樹的平衡性,我們需要一些靈活性,因此在這裡我們允許樹中的一個結點儲存多個鍵。

2-結點:含有一個鍵(及值)和兩條連結,左連結指向的2-3樹中的鍵都小於該結點,右連結指向的2-3樹中的鍵都大於該結點。

3-結點:含有兩個鍵(及值)和三條連結,左連結指向的2-3樹中的鍵都小於該結點,中連結指向的2-3樹中的鍵都位於該結點的兩個鍵之間,右連結指向的2-3樹中的鍵都大於該結點。

(2-3指的是2叉-3叉的意思)



一顆完美平衡的2-3查詢樹中的所有空連結到根結點的距離都是相同的。

查詢

要判斷一個鍵是否在樹中,我們先將它和根結點中的鍵比較。如果它和其中的任何一個相等,查詢命中。否則我們就根據比較的結果找到指向相應區間的連結,並在其指向的子樹中遞迴地繼續查詢。如果這是個空連結,查詢未命中。

插入

要在2-3樹中插入一個新結點,我們可以和二叉查詢樹一樣先進行一次未命中的查詢,然後把新結點掛在樹的底部。但這樣的話樹無法保持完美平衡性。我們使用2-3樹的主要原因就在於它能夠在插入之後繼續保持平衡。

如果未命中的查詢結束於一個2-結點,我們只要把這個2-結點替換為一個3-結點,將要插入的鍵儲存在其中即可。如果未命中的查詢結束於一個3-結點,事情就要麻煩一些。

熱身

先考慮最簡單的例子:只有一個3-結點的樹,向其插入一個新鍵。

這棵樹唯一的結點中已經沒有可插入的空間了。我們又不能把新鍵插在其空結點上(破壞了完美平衡)。為了將新鍵插入,我們先臨時將新鍵存入該結點中,使之成為一個4-結點。建立一個4-結點很方便,因為很容易將它轉換為一顆由3個2-結點組成的2-3樹(如圖所示),這棵樹既是一顆含有3個結點的二叉查詢樹,同時也是一顆完美平衡的2-3樹,其中所有空連結到根結點的距離都相等。


向一個父結點為2-結點的3-結點中插入新鍵

假設未命中的查詢結束於一個3-結點,而它的父結點是一個2-結點。在這種情況下我們需要在維持樹的完美平衡的前提下為新鍵騰出空間。

我們先像剛才一樣構造一個臨時的4-結點並將其分解,但此時我們不會為中鍵建立一個新結點,而是將其移動至原來的父結點中。(如圖所示)


這次轉換也並不影響(完美平衡的)2-3樹的主要性質。樹仍然是有序的,因為中鍵被移動到父結點中去了,樹仍然是完美平衡的,插入後所有的空連結到根結點的距離仍然相同。

向一個父結點為3-結點的3-結點中插入新鍵

假設未命中的查詢結束於一個3-結點,而它的父結點是一個3-結點。

我們再次和剛才一樣構造一個臨時的4-結點並分解它,然後將它的中鍵插入它的父結點中。但父結點也是一個3-結點,因此我們再用這個中鍵構造一個新的臨時4-結點,然後在這個結點上進行相同的變換,即分解這個父結點並將它的中鍵插入到它的父結點中去。

我們就這樣一直向上不斷分解臨時的4-結點並將中鍵插入更高的父結點,直至遇到一個2-結點並將它替換為一個不需要繼續分解的3-結點,或者是到達3-結點的根。


總結

先找插入結點,若結點有空(即2-結點),則直接插入。如結點沒空(即3-結點),則插入使其臨時容納這個元素,然後分裂此結點,把中間元素移到其父結點中。對父結點亦如此處理。(中鍵一直往上移,直到找到空位,在此過程中沒有空位就先搞個臨時的,再分裂。)

★2-3樹插入演算法的根本在於這些變換都是區域性的:除了相關的結點和連結之外不必修改或者檢查樹的其他部分。每次變換中,變更的連結數量不會超過一個很小的常數。所有區域性變換都不會影響整棵樹的有序性和平衡性。

{你確定理解了2-3樹的插入過程了嗎? 如果你理解了,那麼你也就基本理解了紅黑樹的插入}

構造

和標準的二叉查詢樹由上向下生長不同,2-3樹的生長是由下向上的


優點

2-3樹在最壞情況下仍有較好的效能。每個操作中處理每個結點的時間都不會超過一個很小的常數,且這兩個操作都只會訪問一條路徑上的結點,所以任何查詢或者插入的成本都肯定不會超過對數級別

完美平衡的2-3樹要平展的多。例如,含有10億個結點的一顆2-3樹的高度僅在19到30之間。我們最多隻需要訪問30個結點就能在10億個鍵中進行任意查詢和插入操作。

缺點

我們需要維護兩種不同型別的結點,查詢和插入操作的實現需要大量的程式碼,而且它們所產生的額外開銷可能會使演算法比標準的二叉查詢樹更慢。

平衡一棵樹的初衷是為了消除最壞情況,但我們希望這種保障所需的程式碼能夠越少越好。

紅黑二叉查詢樹

【前言:本文所討論的紅黑樹之目的在於使讀者能更簡單清晰地瞭解紅黑樹的構造,使讀者能在紙上清晰快速地畫出紅黑樹,而不是為了寫出紅黑樹的實現程式碼。

若是要在程式碼級理解紅黑樹,則勢必需要記住其複雜的插入和旋轉的各種情況,我認為那只有助於增加大家對紅黑樹的恐懼,實際面試和工作中幾乎不會遇到需要自己動手實現紅黑樹的情況(很多語言的標準庫中就有紅黑樹的實現)。  若對於紅黑樹的C程式碼實現有興趣的,可移步至July的部落格。】

理解紅黑樹一句話就夠了紅黑樹就是用紅連結表示3-結點的2-3樹。那麼紅黑樹的插入、構造就可轉化為2-3樹的問題,即:在腦中用2-3樹來操作,得到結果,再把結果中的3-結點轉化為紅連結即可。而2-3樹的插入,前面已有詳細圖文,實際也很簡單:有空則插,沒空硬插,再分裂。  這樣,我們就不用記那麼複雜且讓人頭疼的紅黑樹插入旋轉的各種情況了。只要清楚2-3樹的插入方式即可。  下面圖文詳細演示。)

紅黑樹的本質

紅黑樹是對2-3查詢樹的改進,它能用一種統一的方式完成所有變換。

替換3-結點

★紅黑樹背後的思想是用標準的二叉查詢樹(完全由2-結點構成)和一些額外的資訊(替換3-結點)來表示2-3樹。

我們將樹中的連結分為兩種型別:紅連結將兩個2-結點連線起來構成一個3-結點,黑連結則是2-3樹中的普通連結。確切地說,我們將3-結點表示為由一條左斜的紅色連結相連的兩個2-結點

這種表示法的一個優點是,我們無需修改就可以直接使用標準二叉查詢樹的get()方法。對於任意的2-3樹,只要對結點進行轉換,我們都可以立即派生出一顆對應的二叉查詢樹。我們將用這種方式表示2-3樹的二叉查詢樹稱為紅黑樹。


紅黑樹的另一種定義是滿足下列條件的二叉查詢樹:

⑴紅連結均為左連結。

⑵沒有任何一個結點同時和兩條紅連結相連。

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

如果我們將一顆紅黑樹中的紅連結畫平,那麼所有的空連結到根結點的距離都將是相同的。如果我們將由紅連結相連的結點合併,得到的就是一顆2-3樹。

相反,如果將一顆2-3樹中的3-結點畫作由紅色左連結相連的兩個2-結點,那麼不會存在能夠和兩條紅連結相連的結點,且樹必然是完美平衡的。


無論我們用何種方式去定義它們,紅黑樹都既是二叉查詢樹,也是2-3

(2-3樹的深度很小,平衡性好,效率高,但是其有兩種不同的結點,實際程式碼實現比較複雜。而紅黑樹用紅連結表示2-3樹中另類的3-結點,統一了樹中的結點型別,使程式碼實現簡單化,又不破壞其高效性。)

顏色表示

因為每個結點都只會有一條指向自己的連結(從它的父結點指向它),我們將連結的顏色儲存在表示結點的Node資料型別的布林變數color中(若指向它的連結是紅色的,那麼該變數為true,黑色則為false)。

當我們提到一個結點顏色時,我們指的是指向該結點的連結的顏色。

旋轉

在我們實現的某些操作中可能會出現紅色右連結或者兩條連續的紅連結,但在操作完成前這些情況都會被小心地旋轉並修復。

(我們在這裡不討論旋轉的幾種情況,把紅黑樹看做2-3樹,自然可以得到正確的旋轉後結果)

插入

在插入時我們可以使用旋轉操作幫助我們保證2-3樹和紅黑樹之間的一一對應關係,因為旋轉操作可以保持紅黑樹的兩個重要性質:有序性完美平衡性

熱身

向2-結點中插入新鍵

(向紅黑樹中插入操作時,想想2-3樹的插入操作。紅黑樹與2-3樹在本質上是相同的,只是它們對3結點的表示不同。

向一個只含有一個2-結點的2-3樹中插入新鍵後,2-結點變為3-結點。我們再把這個3-結點轉化為紅結點即可)


向一顆雙鍵樹(即一個3-結點)中插入新鍵

(向紅黑樹中插入操作時,想想2-3樹的插入操作。你把紅黑樹當做2-3樹來處理插入,一切都變得簡單了)

(向2-3樹中的一個3-結點插入新鍵,這個3結點臨時成為4-結點,然後分裂成3個2結點)


★一顆紅黑樹的構造全過程


平衡二叉樹(AVL樹)

定義:平衡二叉樹(Balance Binary Tree)又稱AVL樹。它或者是一顆空樹,或者是具有下列性質的二叉樹:它的左子樹和右子樹都是平衡二叉樹,且左子樹和右子樹的深度之差的絕對值不超過1。

若將二叉樹上結點的平衡因子BF(BalanceFactor)定義為該結點的左子樹深度減去它的右子樹深度,則平衡因子的絕對值大於1

其旋轉操作 用2-3樹的分裂來類比想象。