查找(一)史上最簡單清晰的紅黑樹講解


原文地址:http://blog.csdn.net/yang_yulei/article/details/26066409#t4


查找(一)



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

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

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


二分查找


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

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

 

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

 

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

 

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

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

 

二叉查找樹


一顆二叉查找樹(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樹的分裂來類比想象。




下半部分——散列表、B樹、B+樹、Trie樹。


發佈了64 篇原創文章 · 獲贊 10 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章