說在前面
個人讀書筆記
查找
所謂的查找或搜索(search),指從一組數據對象中找出符合特定條件者,這是構建算法的一種基本而重要的操作。其中的數據對象,統一地表示和實現爲詞條(entry)的形式;不同詞條之間,依照各自的關鍵碼(key)彼此區分。根據身份證號查找特定公民,根據車牌號查找特定車輛,根據國際統一書號查找特定圖書,均屬於根據關鍵碼查找特定詞條的實例。
查找與數據對象的物理位置或邏輯次序均無關。實際上,查找的過程與結果,僅僅取決於目標對象的關鍵碼,故這種方式亦稱作循關鍵碼訪問(call-by-key)。
詞條對象擁有成員變量key和value。前者作爲特徵,是詞條之間比對和比較的依據;後者爲實際的數據。
詞條的判等與比較等操作可以通過關鍵碼的判等與比較實現。當然,這裏隱含地做了一個假定——所有詞條構成一個全序關係,可以相互比對和比較。需指出的是,這一假定條件不見得總是滿足。比如在人事數據庫中,作爲姓名的關鍵碼之間並不具有天然的大小次序。另外,在任務相對單純但更加講求效率的某些場合,並不允許花費過多時間來維護全序關係,只能轉而付出有限的代價維護一個偏序關係。
搜索樹
高效率的動態修改兼顧高效率的靜態查找
二叉搜索樹
在所謂的二叉搜索樹(binary search tree)中,處處都滿足順序性:
任一節點r的左(右)子樹中,所有節點(若存在)均不大於(不小於)r
任何一棵二叉樹是二叉搜索樹,當且僅當其中序遍歷序列單調非降:
查找
二叉搜索樹的查找算法,亦採用了減而治之的思路與策略,其執行過程可描述爲:
從樹根出發,逐步地縮小查找範圍,直到發現目標(成功)或縮小至空樹(失敗)
如上圖查找關鍵碼22的過程:
首先,經與根節點16比較確認目標關鍵碼更大,故深入右子樹25遞歸查找;經比較發現目標關鍵碼更小,故繼續深入左子樹19遞歸查找;經再次比較確認目標關鍵碼更大後,深入右子樹22遞歸查找;最終在節點22處匹配,查找成功。
一般地,在上述查找過程中,一旦發現當前節點爲NULL,即說明查找範圍已經縮小至空,查找失敗;否則,視關鍵碼比較結果,向左(更小)或向右(更大)深入,或者報告成功(相等)。
上面是在子樹中查找關鍵碼的算法。節點的插入和刪除操作,都需要首先調用查找算法,並根據查找結果確定後續的處理方式。因此,這裏以引用方式傳遞(子)樹根節點,以爲後續操作提供必要的信息。所謂引用傳遞是指在調用函數時將實際參數的地址傳遞到函數中,那麼在函數中對參數所進行的修改,將影響到實際參數。
當二叉搜索樹退化爲(接近於)一條單鏈時,此時的單次查找可能需要線性時間,因爲實際上這樣的一棵“二分”搜索樹,已經退化成了一個不折不扣的一維有序列表,而此時的查找則等效於順序查找。
由此我們可得到啓示:
若要控制單次查找在最壞情況下的運行時間,須從控制二叉搜索樹的高度入手
插入
以如上圖中所示的二叉搜索樹爲例。若欲插入關鍵碼40,則在執行search(40)之後,如圖所示,_hot將指向比較過的最後一個節點46,同時返回其左孩子(此時爲空)的位置。於是接下來如圖所示,只需創建新節點40,並將其作爲46的左孩子接入,拓撲意義上的節點插入即告完成。
不過,爲保持二叉搜索樹作爲數據結構的完整性和一致性,還需從節點_hot(46)出發,自底而上地逐個更新新節點40歷代祖先的高度。
接下來若欲插入關鍵碼55,則在執行search(55)之後如圖所示,_hot將指向比較過的最後一個節點53,同時返回其右孩子(此時爲空)的位置。於是如圖所示,創建新節點55,並將其作爲53的右孩子接入。當然,此後同樣需從節點_hot出發,逐代更新祖先的高度。
注意,按照以上實現方式,無論插入操作成功與否,都會返回一個非空位置,且該處的節點與擬插入的節點相等。如此可以確保一致性,以簡化後續的操作。
刪除
以上圖中所示二叉搜索樹爲例。若欲刪除節點69,需首先通過search(69)定位待刪除節點(69)。因該節點的右子樹爲空,故只需如圖所示,將其替換爲左孩子(64),則拓撲意義上的節點刪除即告完成。
當然,爲保持二叉搜索樹作爲數據結構的完整性和一致性,還需更新全樹的規模記錄,釋放被摘除的節點(69),並自下而上地逐個更新替代節點(64)歷代祖先的高度。
注意,首個需要更新高度的祖先(58),恰好由_hot指示。
不難理解,對於沒有左孩子的目標節點,也可以對稱地予以處理。當然,以上同時也已涵蓋了左、右孩子均不存在(即目標節點爲葉節點)的情況。
那麼,當目標節點的左、右孩子雙全時,刪除操作又該如何實施呢?
繼續上例,設擬再刪除二度節點36。如圖所示,首先找到該節點的直接後繼(40)。然後,只需如圖所示交換二者的數據項,則可將後繼節點等效地視作待刪除的目標節點。不難驗證,該後繼節點必無左孩子,從而相當於轉化爲此前相對簡單的情況。於是最後可如圖所示,將新的目標節點(36)替換爲其右孩子(46)。
請注意,在中途互換數據項之後,這一局部如圖所示曾經一度並不滿足順序性。但這並不要緊——不難驗證,在按照上述方法完成整個刪除操作之後,全樹的順序性必然又將恢復。
同樣地,除了更新全樹規模記錄和釋放被摘除節點,此時也要更新一系列祖先節點的高度。
不難驗證,此時首個需要更新高度的祖先(53),依然恰好由_hot指示。
平衡二叉樹
既然二叉搜索樹的性能主要取決於高度,節點數目固定的前提下,應儘可能地降低高度。
相應地,應儘可能地使兄弟子樹的高度彼此接近,即全樹儘可能地平衡。當然,包含n個節點的二叉樹,高度不可能小於。若樹高恰好爲,則稱作理想平衡樹。
在漸進意義下適當放鬆標準之後的平衡性,稱作適度平衡。
若將樹高限制爲“漸進地不超過”,則AVL樹、伸展樹、紅黑樹、kd-樹等,都屬於適度平衡。這些變種,因此也都可歸入平衡二叉搜索樹(balanced binary search tree,BBST)之列。
等價二叉搜索樹
若兩棵二叉搜索樹的中序遍歷序列相同,則稱它們彼此等價;反之亦然。
由該圖也不難看出,雖然等價二叉搜索樹中各節點的垂直高度可能有所不同,但水平次序完全一致。這一特點可概括爲“上下可變,左右不亂”。
平衡二叉搜索樹的適度平衡性,都是通過對樹中每一局部增加某種限制條件來保證的。比如,在紅黑樹中,從樹根到葉節點的通路,總是包含一樣多的黑節點;在AVL樹中,兄弟節點的高度相差不過1。事實上,這些限制條件設定得非常精妙,除了適度平衡性,還具有如下局部性:
- 經過單次動態修改操作後,至多隻有處局部不再滿足限制條件
- 總可在時間內,使這處局部(以至全樹)重新滿足限制條件
這就意味着:
剛剛失去平衡的二叉搜索樹,必然可以迅速轉換爲一棵等價的平衡二叉搜索樹。
等價二叉搜索樹之間的上述轉換過程,也稱作等價變換。
最基本的修復失衡二叉搜索樹手段,就是通過圍繞特定節點的旋轉,實現等價前提下的局部拓撲調整。
zig和zag
如上圖所示,設和是的左孩子、右子樹,和是的左、右子樹。所謂以爲軸的zig旋轉,即如上圖所示,重新調整這兩個節點與三棵子樹的聯接關係:
將和作爲的左子樹、右孩子,和分別作爲的左、右子樹。
可見,儘管局部結構以及子樹根均有變化,中序遍歷序列仍是,故zig旋轉屬於等價變換。
對稱地如上圖所示,設和是的左子樹、右孩子,和分別是的左、右子樹。所謂以爲軸的zag旋轉,即如上圖所示,重新調整這兩個節點與三棵子樹的聯接關係:
將和作爲的左孩子、右子樹,和分別作爲的左、右子樹。
同樣地,旋轉之後中序遍歷序列依然不變,故zag旋轉亦屬等價變換。
zig和zag旋轉均屬局部操作,僅涉及常數個節點及其之間的聯接關係,故均可在常數時間內完成。正因如此,在實現各種二叉搜索樹平衡化算法時,它們都是支撐性的基本操作。
就與樹相關的指標而言,經一次zig或zag旋轉之後,節點的深度加一,節點的深度減一;這一局部子樹(乃至全樹)的高度可能發生變化,但上、下幅度均不超過一層。
AVL樹
通過合理設定適度平衡的標準,並藉助以上等價變換,AVL樹(AVL tree)可以實現近乎理想的平衡。在漸進意義下,AVL樹可始終將其高度控制在以內,從而保證每次查找、插入或刪除操作,均可在的時間內完成。
任一節點的平衡因子(balance factor)定義爲“其左、右子樹的高度差”,即:
請注意,空樹高度取,單節點子樹(葉節點)高度取,與以上定義沒有衝突。
所謂AVL樹,即平衡因子受限的二叉搜索樹——其中各節點平衡因子的絕對值均不超過1
在完全二叉樹中各節點的平衡因子非0即1,故完全二叉樹必是AVL樹
失衡與重平衡
AVL樹與常規的二叉搜索樹一樣,也應支持插入、刪除等動態修改操作。但經過這類操作之後,節點的高度可能發生變化,以致於不再滿足AVL樹的條件。
以插入操作爲例,考查上圖中的AVL樹,其中的關鍵碼爲字符類型。現插入關鍵碼,於是如圖所示,節點、 和都將失衡。類似地,摘除關鍵碼之後,也會如圖所示導致節點的失衡。
如此因節點的插入或刪除而暫時失衡的節點,構成失衡節點集,記作。請注意,若爲被摘除的節點,則僅含單個節點;但若爲被引入的節點,則可能包含多個節點。
節點插入
不難看出,新引入節點後,中的節點都是的祖先,且高度不低於的祖父。以下,將其中的最深者記作。在與之間的通路上,設爲的孩子,爲的孩子。注意,既然不低於的祖父,則必是的真祖先。
首先,需要找到如上定義的。爲此,可從出發沿指針逐層上行並覈對平衡因子,首次遇到的失衡祖先即爲。既然原樹是平衡的,故這一過程只需時間。
根據節點、和之間具體的聯接方向,將採用不同的局部調整方案:
如上圖所示,設是的右孩子,且是的右孩子。這種情況下,必是由於在子樹中剛插入某節點,而使不再平衡。圖中以虛線聯接的每一對灰色方塊中,其一對應於節點,另一爲空。
此時,可做逆時針旋轉zag(),得到如圖所示的另一棵等價二叉搜索樹。
可見,經如此調整之後,必將恢復平衡。不難驗證,通過zig()可以處理對稱的失衡。
如上圖所示,設節點是的左孩子,而是的右孩子。這種情況,也必是由於在子樹中插入了新節點,而致使不再平衡。同樣地,在圖中以虛線聯接的每一對灰色方塊中,其一對應於新節點,另一爲空。
此時,可先做順時針旋轉zig(),得到如圖所示的一棵等價二叉搜索樹。再做逆時針旋轉zag(),得到如圖所示的另一棵等價二叉搜索樹。
此類分別以父子節點爲軸、方向互逆的連續兩次旋轉,合稱“雙旋調整”。可見,經如此調整之後,亦必將重新平衡。不難驗證,通過zag()和zig()可以處理對稱的情況。
無論單旋或雙旋,經局部調整之後,不僅能夠重獲平衡,而且局部子樹的高度也必將復原。這就意味着,以上所有祖先的平衡因子亦將統一地復原——換而言之,在AVL樹中插入新節點後,僅需不超過兩次旋轉,即可使整樹恢復平衡。
AVL樹的節點插入操作可以在時間內完成。
節點刪除
與插入操作十分不同,在摘除節點後,以及隨後的調整過程中,失衡節點集始終至多隻含一個節點。而且若該節點存在,其高度必與失衡前相同。
另外還有一點重要的差異是,有可能就是的父親。
與插入操作同理,從_hot節點出發沿指針上行,經過時間即可確定位置。作爲失衡節點的,在不包含的一側,必有一個非空孩子,且的高度至少爲1。於是,可按以下規則從的兩個孩子(其一可能爲空)中選出節點:
若兩個孩子不等高,則取作其中的更高者;否則,優先取與同向者(亦即,與同爲左孩子,或者同爲右孩子)。
以下不妨假定失衡後的平衡因子爲+2(爲-2的情況完全對稱)。根據祖孫三代節點、和的位置關係,通過以和爲軸的適當旋轉,同樣可以使得這一局部恢復平衡。
如上圖所示,由於在中刪除了節點而致使不再平衡,但的平衡因子非負時,通過以爲軸順時針旋轉一次即可恢復局部的平衡。平衡後的局部子樹如圖所示。
同樣地這裏約定,圖中以虛線聯接的灰色方塊所對應的節點,不能同時爲空;底部的灰色方塊所對應的節點,可能爲空,也可能非空。
如上圖所示,失衡時若的平衡因子爲-1,則經過以爲軸的一次逆時針旋轉之後(圖),接着再以爲軸順時針旋轉,即可恢復局部平衡(圖)。
對於刪除節點操作,恢復平衡之後,局部子樹的高度可能降低。對於插入節點操作,重平衡後不僅能恢復子樹的平衡性,也同時能恢復子樹的高度。
與插入操作不同,在刪除節點之後,儘管也可通過單旋或雙旋調整使局部子樹恢復平衡,但就全局而言,依然可能再次失衡。對於刪除節點操作,若原本屬於某一更高祖先的更短分支,則因爲該分支現在又進一步縮短,從而會致使該祖先失衡。在摘除節點之後的調整過程中,這種由於低層失衡節點的重平衡而致使其更高層祖先失衡的現象,稱作“失衡傳播”。
失衡傳播的方向必然自底而上,而不致於影響到後代節點。在此過程中的任一時刻,至多隻有一個失衡的節點;高層的某一節點由平衡轉爲失衡,只可能發生在下層失衡節點恢復平衡之後。因此,可沿指針逐層遍歷所有祖先,每找到一個失衡的祖先節點,即可套用以上方法使之恢復平衡。
統一重平衡算法——“3 + 4”重構
無論對於插入或刪除操作,從剛發生修改的位置出發逆行而上,直至遇到最低的失衡節點。於是在更高一側的子樹內,其孩子節點和孫子節點必然存在,而且這一局部必然可以、和爲界,分解爲四棵子樹——將它們按中序遍歷次序重命名爲 至。
若同樣按照中序遍歷次序,重新排列、和,並將其命名爲、和,則這一局部的中序遍歷序列應爲:
{}
將這三個節點與四棵子樹重新如上“組裝”起來,恰好即是一棵AVL樹。
結語
如果您有修改意見或問題,歡迎留言或者通過郵箱和我聯繫。
手打很辛苦,如果我的文章對您有幫助,轉載請註明出處。