數據結構與算法-二叉查找樹平衡(DSW)

上一節探討了二叉查找樹的基本操做,二叉查找樹的查找效率在理想狀態下是O(lgn),使用該樹進行查找老是比鏈表快得多。可是,該論點並不老是正確,由於查找效率和二叉樹的形狀息息相關。就像這樣:算法


圖1-1給出了3顆二叉查找樹,它們存儲着相同的數據,但很明顯,圖1-1(A)的樹是最好的。在最壞的狀況下,圖A定位一個對象須要3次測試,圖C須要6次。緣由在於,圖C的數據不是平均分佈的,該樹實際上已經退化成一個鏈表,已經失去了二叉查找樹的優越性。數組

這裏須要引入一個新的概念,叫作平衡。若是樹中任一節點的兩個子樹的高度差爲0或者1,該二叉樹就是高度平衡的或者簡稱平衡的。例如,圖B中的節點20,其子樹的高度差是1,這是能夠接受的。 可是對於節點10,其子樹的高度差是3,這意味着整棵樹是不平衡的。另外,若是樹是平衡的,而且該樹全部葉節點都出如今一個或者兩個層次上,那麼該樹是徹底平衡的。數據結構

那麼問題來了,如何獲得一顆平衡的二叉查找樹?post

許多技術均可以適當的平衡二叉樹。一些技術對數據從新排序從而建立一顆平衡的二叉樹,另外一些技術在因爲插入或者刪除元素而致使樹不平衡時,會從新平衡樹。咱們首先來探討如何建立一顆平衡二叉查找樹,而後介紹如何從新平衡已有的二叉查找樹。測試

想要建立一顆平衡二叉查找樹,首先要觀察這種樹的特性,根據觀察到的規律總結出來的數學邏輯,其實就是算法。就像下面:spa


發現了嗎?若是將一顆完美的平衡的二叉查找樹壓平,將數據線性列出,你會發現它是有序的,而且根節點30處於數組A中間的位置。不止如此,根節點30的左子樹的根節點20處於數組B的中間位置,根節點30的右子樹的根節點47處於數組C的中間位置。以此類推,全部子樹的根節點老是處於某個數組的中間位置。這很相似二分查找的邏輯,而且你是否發覺,數組A實際上是該二叉樹中序遍歷的結果。3d

這裏有個定論:二叉查找樹的中序遍歷能夠獲得有序的數據流指針

證實也很容易,這裏使用天然語言簡單描述下:code

假如要對一顆二叉查找樹進行中序遍歷,首先將其分解成根節點,左子樹,右子樹。由於中序遍歷的邏輯是首先遍歷左子樹,而後是根節點,最後是右子樹。咱們能夠將它們放到一個棧中,大概長得是這個樣子:cdn


根據二叉查找樹的定義,左子樹全部節點小於根節點,右子樹全部節點大於根節點。所以,棧此時的狀態是有序的。最後,咱們將棧中的子樹所有分解成新的根節點、左子樹、右子樹,而且按照上圖所示的順序放入棧中,直到棧中再也不存在子樹,所有分解成了根節點。最後是這個樣子的:


棧中保存的數據流就是中序遍歷的結果。因爲從分解開始是有序的,而且隨後的每一次分解都是有序的,最終造成的數據流也是有序的。

這就證實了上面的定論: 二叉查找樹的中序遍歷能夠獲得有序的數據流

好吧,說了這麼多,實際上是在總結咱們觀察平衡二叉查找樹找到的規律:

將平衡二叉查找樹進行中序遍歷,能夠獲得一個有序的數據流,而且根節點處於數據流的中間位置。以此類推,右子樹的根節點處於右子樹數據流的中間位置,左子樹的根節點處於左子樹數據流的中間位置。

根據以上規律,咱們能夠獲得一個建立平衡二叉查找樹的算法,首先用天然語言進行描述:

假設咱們有一個有序的數組,數組中元素個數爲n。咱們能夠將數組中間元素指定爲根,這個數組如今包含兩個子數組:一個包含從數組的開始到剛剛選爲根的元素之間的全部元素,另外一個包含剛剛選爲根的元素到數組的末尾之間的全部元素。根的左子節點指定爲第一個子數組的中間元素,根的右子節點指定爲第二個子數組的中間元素。以此類推,數組中的每一個元素均可以放到二叉樹中,最終造成的二叉樹就是一顆平衡二叉查找樹。

代碼以下:

void balance(int data[], int first, int last) { 
        if (first <= last) {
                int middle = (first + last)/2;         
                insert(data[middle]);
                balance(data, first, middle - 1);
                balance(data, middle + 1, last);   
        }
}複製代碼

代碼中使用了遞歸,遞歸可使程序邏輯變得簡單,可是會加大運行時棧的負擔,慎用。咱們這裏只是探討算法,所以,使用遞歸實現是能夠的。

該算法存在嚴重的缺陷:在建立樹以前,全部的數據都必須放在數組中。當必須使用樹,可是準備保存到樹中的數據仍然在輸入的時候,該算法就不太合適了。咱們可使用折中的方法,若是數據在持續輸入,咱們能夠按照建立二叉查找樹的方法,將數據保存到二叉樹中。數據輸入完畢以後,只須要對該樹進行中序遍歷,就能夠獲得有序的數據流,而後使用上述的算法,就能夠獲得一顆平衡的二叉查找樹。

上訴討論的算法效率有點低,由於在建立徹底平衡的樹以前,須要使用一個額外的有序數組。爲了不排序,這一算法須要破壞樹並用中序遍歷把元素放在數組中,而後重建該樹,這樣作效率並不高,除非樹很小。然而,存在幾乎不須要存儲中間變量也不須要排序過程的算法。這就是DSW算法。該算法能夠對已經存在的二叉查找樹進行平衡,而且不須要中間變量。

老規矩,在坐享其成的獲取DSW算法以前,咱們先本身分析一下如何將一顆二叉查找樹進行平衡。先思考一個問題:


如何將上圖中的二叉查找樹從新構建成一顆平衡的二叉查找樹,爲了方便找到規律,這裏給出平衡以後的二叉查找樹:


是否是發現很類似?若是將平衡以後的二叉查找樹從右上角到左下角壓平,能夠獲得和原始二叉查找樹類似的結構。那麼,經過哪些操做能夠將原始二叉樹轉變成平衡二叉樹呢?咱們首先觀察左上角的4個節點:五、十、20、15。能夠屏蔽掉其餘節點,把它們當作不存在。若是你瞭解二叉樹節點的左旋操做,立刻就能明白,只要將20節點圍繞其父節點10左旋轉,立刻就能夠獲得平衡以後的二叉樹。什麼是左旋?左旋有什麼做用?咱們首先來探討左旋的做用,而後探討左旋的原理。左旋能夠提高根節點左子樹的高度,下降根節點右子樹的高度,而且左旋以後依然是二叉查找樹。好比,五、十、20、15。10做爲根節點,左子樹高度爲1,右子樹高度爲2。通過左旋以後,20成爲新的根節點,左子樹高度提高爲2,右子樹高度下降爲0。能夠發現,左旋以後的二叉樹依然是二叉查找樹。那麼左旋究竟是什麼呢?左旋其實與二叉樹節點的合併刪除算法很是類似,而且原理是一致的,在數據結構與算法-二叉查找樹這篇文章中,詳細講解了合併刪除的原理。這裏簡單描述下左旋的操做以及原理。依然以五、十、20、15節點爲例,左旋是針對右子樹的根節點來講的,對稱的右旋是針對左子樹的根節點來講的。在這裏,20節點做爲10節點的右子節點,能夠圍繞10節點進行左旋操做。首先,將根節點10以及左子樹做爲A組,將20節點所在的右子樹做爲B組,左旋就是將A組合併到B組上。將A組設置爲20節點的左子樹,將20節點原有的左子樹設置爲A組的右子樹。就像這樣:


將A組合併到B組,原則上來說,只要20節點到10節點的路徑中不存在右指針便可,由於,一旦出現右指針,就意味着,B組中存在節點小於A組節點,可是咱們都知道,A組是二叉查找樹的根節點以及左子樹組成,全部節點都小於B組節點(右子樹)。在左旋操做中,將A組設置爲20節點的左子樹,只有一個左指針,所以,該操做是合法的。20節點原有的左子樹須要合併到A組上,原則上來說,只要10節點到15節點的路徑中不存在左指針便可,原理和上訴相似。由於10的右子樹爲空,因此這裏就將15節點直接設置爲10節點的右子樹。到此爲止,左旋操做完畢,由於左旋操做本質是二叉查找樹中合法的子樹合併操做,因此最後的二叉樹也是合法的二叉查找樹,可是左旋提升了左子樹的高度,下降了右子樹的高度,左旋和右旋是對稱的,有興趣的能夠自行了解。將目光放到平衡二叉查找樹上,比較平衡以前的二叉樹以及平衡以後的二叉樹。能夠發現,只要進行兩步操做就能實現轉變。第一步,分別對20節點、30節點、49節點進行左旋操做。第二步,繼續對30節點進行左旋操做。搞定收工,下面總結通用算法。

若是忽略最後一層的葉子節點,剩餘的二叉樹是一個完美二叉樹的線性排列。那麼,該完美二叉樹的元素個數是多少呢?咱們假設原二叉查找樹元素個數爲n,完美二叉樹的高度爲h,那麼能夠獲得不等式2^h - 1 <= n,即h <= lg(n+1),只要不等式向下取整,就能夠獲取到完美二叉樹的高度。那麼,完美二叉樹元素個數m爲2^h - 1。一眼就能看出m是個奇數,而且在第一次左旋時,是從上到下第二個節點開始的,作多少次左旋呢?實際上是m/2次。帶入到上述二叉查找樹中,完美二叉樹高度h爲lg(12 + 1),向下取整爲3,完美二叉樹元素個數m爲2^h - 1,即7,第一次左旋次數爲m/2,即3。上述平衡二叉查找樹過程當中,一共有兩步,第一步作了3次左旋,第二步作了1次左旋。能夠發現,根據不一樣高度(h)的完美二叉樹,須要作不一樣的步數(p),它們的關係是p = h - 1。固然,終止條件也能夠是另外一種。能夠發現,假設完美二叉樹元素個數爲m,那麼第一步左旋次數爲m/2,記爲m1,第二步左旋次數爲m1/2,記爲m2,以此類推,若是m(n)小於1,證實已經平衡完畢。

到此爲止,咱們已經總結出了平衡二叉查找樹的關鍵邏輯。問題從平衡二叉查找樹轉變成了如何獲取相似下圖的二叉樹?


能夠發現,二叉查找樹總的元素個數n爲12,從右上到左下,最外層的節點數m是7。發現了嗎?其實7就是該二叉查找樹包含的完美二叉樹元素個數。經過lg(n + 1)向下取整能夠獲得完美二叉樹的高度h,經過2^h - 1能夠獲得完美二叉樹元素個數m。將12帶入公式,能夠獲得h爲3,m爲7。內層有5個節點五、1五、2三、2八、40,若是分別對它們進行右旋操做,能夠獲得如下圖形:


是否是很熟悉?這不就是二叉查找樹最差的鏈表形式嘛!真是造化弄人,咱們在平衡二叉查找樹的過程當中,居然還須要藉助二叉樹的鏈表形式。從鏈表形式的第二個節點開始,每次隔一個節點進行n - m次左旋,即5次左旋,就能獲得咱們想要的二叉樹形式。如今問題又轉變成了,如何從一個普通的二叉查找樹獲取到最差的鏈表形式?答案是從根節點開始,沿着右子樹,不停的右旋(提升右子樹高度,下降左子樹高度),直至全部左指針爲空。就像下面動圖:


到目前爲止,咱們已經探討出從普通二叉查找樹到平衡二叉查找樹的過程。其實這就是DSW算法。總結以下:

  • 建立主鏈

建立主鏈就是從普通二叉查找樹轉變到只有右指針的鏈式結構,僞代碼以下:

createMainChain(root) {
        tmp = root;     
        while(tmp != 0) {
                if tmp有左子節點
                        圍繞tmp右旋左子節點;             //這樣左子節點將成爲tmp的父節點
                        tmp設置爲剛剛成爲父節點的子節點;
                else  將tmp設置爲它的右子節點; 
        }
}複製代碼
能夠發現,建立主鏈的過程就是不停的右旋,直至二叉樹中不存在左指針。
  • 主鏈轉換成平衡樹

主鏈轉換成平衡樹其實有兩步,第一步就是從主鏈轉變成相似下面的圖形:


二叉樹一共有n(12)個節點,外層節點數n(7)是該二叉樹包含的完美二叉樹個數,計算方式在上面已經探討過,就是lg(n + 1)向下取整獲取完美二叉樹高度h(3),而後由2^h - 1獲取到完美二叉樹個數m(7),那麼內層葉子節點個數就是n - m,即5個。咱們只要從主鏈的第二個節點開始,每隔一個節點進行一次左旋,一共進行n - m次(5次)便可。

第二步就是將以上圖形轉變成平衡二叉查找樹,算法邏輯已經探討過了。其實就是將外層節點從第二個開始,每隔一個節點進行一次左旋,一共進行m/2次,記爲m1。這時,最外層節點個數成爲m1,繼續從第二個開始,每隔一個節點進行一次左旋,一共進行m1/2次,記爲m2。以此類推,終止條件是m(n)小於1,或者是循環h - 1次。

算法中的邏輯,也就是爲何這麼作?所有已經在上面探討清楚,能夠反覆閱讀加深理解。

第二步的邏輯總結一下就是不停的左旋(提升左子樹高度,下降右子樹高度),直至二叉查找樹平衡。

僞代碼以下:

createBalanceTree() {     
        n = 節點數;     
        m = 2^h - 1;    
        從主鏈的頂部第二個節點開始,每隔一個節點進行左旋,一共進行n - m次; 
        while(m > 1) {
                m = m/2;
                從主鏈的頂部第二個節點開始,每隔一個節點進行左旋,一共進行m次;       
        }
}複製代碼

DSW算法已經探討完畢。DSW算法是對已有的二叉查找樹進行全局平衡的算法,二叉樹中的每一個節點都有可能改變位置。它的算法複雜度是O(n),這一時間隨着n線性增加,並且只須要很小且固定的存儲空間。總之,DSW是一個很是優秀的對二叉樹進行全局平衡的算法。

到目前爲止,咱們探討了從數組中如何建立一顆平衡的二叉查找樹以及如何對已有的普通二叉查找樹進行全局平衡。可是,還有一個問題沒有解決,二叉查找樹之因此不平衡,一般是因爲插入或者刪除操做形成的。這種不平衡一般是局部不平衡,這種時候不須要使用DSW算法對全局進行平衡。從新平衡只須要在局部進行便可,這就是大名鼎鼎的AVL樹,這是咱們下節須要探討的內容。

數據結構與算法-二叉查找樹平衡(AVL)

相關文章
相關標籤/搜索