【程序人生】數據結構雜記(七)

說在前面

個人讀書筆記

查找

所謂的查找或搜索(search),指從一組數據對象中找出符合特定條件者,這是構建算法的一種基本而重要的操作。其中的數據對象,統一地表示和實現爲詞條(entry)的形式;不同詞條之間,依照各自的關鍵碼(key)彼此區分。根據身份證號查找特定公民,根據車牌號查找特定車輛,根據國際統一書號查找特定圖書,均屬於根據關鍵碼查找特定詞條的實例。

查找與數據對象的物理位置或邏輯次序均無關。實際上,查找的過程與結果,僅僅取決於目標對象的關鍵碼,故這種方式亦稱作循關鍵碼訪問(call-by-key)。

詞條對象擁有成員變量key和value。前者作爲特徵,是詞條之間比對和比較的依據;後者爲實際的數據。

詞條的判等與比較等操作可以通過關鍵碼的判等與比較實現。當然,這裏隱含地做了一個假定——所有詞條構成一個全序關係,可以相互比對和比較。需指出的是,這一假定條件不見得總是滿足。比如在人事數據庫中,作爲姓名的關鍵碼之間並不具有天然的大小次序。另外,在任務相對單純但更加講求效率的某些場合,並不允許花費過多時間來維護全序關係,只能轉而付出有限的代價維護一個偏序關係

搜索樹

高效率的動態修改兼顧高效率的靜態查找

二叉搜索樹

在所謂的二叉搜索樹(binary search tree)中,處處都滿足順序性:
任一節點r的左(右)子樹中,所有節點(若存在)均不大於(不小於)r
在這裏插入圖片描述
任何一棵二叉樹是二叉搜索樹,當且僅當其中序遍歷序列單調非降
在這裏插入圖片描述

查找

二叉搜索樹的查找算法,亦採用了減而治之的思路與策略,其執行過程可描述爲:
從樹根出發,逐步地縮小查找範圍,直到發現目標(成功)或縮小至空樹(失敗)

在這裏插入圖片描述
如上圖查找關鍵碼22的過程:
首先,經與根節點16比較確認目標關鍵碼更大,故深入右子樹25遞歸查找;經比較發現目標關鍵碼更小,故繼續深入左子樹19遞歸查找;經再次比較確認目標關鍵碼更大後,深入右子樹22遞歸查找;最終在節點22處匹配,查找成功。

一般地,在上述查找過程中,一旦發現當前節點爲NULL,即說明查找範圍已經縮小至空,查找失敗;否則,視關鍵碼比較結果,向左(更小)或向右(更大)深入,或者報告成功(相等)。

在這裏插入圖片描述

上面是在子樹vv中查找關鍵碼ee的算法。節點的插入和刪除操作,都需要首先調用查找算法,並根據查找結果確定後續的處理方式。因此,這裏以引用方式傳遞(子)樹根節點,以爲後續操作提供必要的信息。所謂引用傳遞是指在調用函數時將實際參數的地址傳遞到函數中那麼在函數中對參數所進行的修改,將影響到實際參數

當二叉搜索樹退化爲(接近於)一條單鏈時,此時的單次查找可能需要線性時間,因爲實際上這樣的一棵“二分”搜索樹,已經退化成了一個不折不扣的一維有序列表,而此時的查找則等效於順序查找。
由此我們可得到啓示:
若要控制單次查找在最壞情況下的運行時間,須從控制二叉搜索樹的高度入手

插入

在這裏插入圖片描述
以如上圖中(a)(a)所示的二叉搜索樹爲例。若欲插入關鍵碼40,則在執行search(40)之後,如圖(b)(b)所示,_hot將指向比較過的最後一個節點46,同時返回其左孩子(此時爲空)的位置。於是接下來如圖(c)(c)所示,只需創建新節點40,並將其作爲46的左孩子接入,拓撲意義上的節點插入即告完成。

不過,爲保持二叉搜索樹作爲數據結構的完整性和一致性,還需從節點_hot(46)出發,自底而上地逐個更新新節點40歷代祖先的高度

接下來若欲插入關鍵碼55,則在執行search(55)之後如圖(c)(c)所示,_hot將指向比較過的最後一個節點53,同時返回其右孩子(此時爲空)的位置。於是如圖(d)(d)所示,創建新節點55,並將其作爲53的右孩子接入。當然,此後同樣需從節點_hot出發,逐代更新祖先的高度。

在這裏插入圖片描述
注意,按照以上實現方式,無論插入操作成功與否,都會返回一個非空位置,且該處的節點與擬插入的節點相等。如此可以確保一致性,以簡化後續的操作。

刪除

在這裏插入圖片描述
以上圖中(a)(a)所示二叉搜索樹爲例。若欲刪除節點69,需首先通過search(69)定位待刪除節點(69)。因該節點的右子樹爲空,故只需如圖(b)(b)所示,將其替換爲左孩子(64),則拓撲意義上的節點刪除即告完成。
當然,爲保持二叉搜索樹作爲數據結構的完整性和一致性,還需更新全樹的規模記錄,釋放被摘除的節點(69),並自下而上地逐個更新替代節點(64)歷代祖先的高度

注意,首個需要更新高度的祖先(58),恰好由_hot指示。

不難理解,對於沒有左孩子的目標節點,也可以對稱地予以處理。當然,以上同時也已涵蓋了左、右孩子均不存在(即目標節點爲葉節點)的情況。

那麼,當目標節點的左、右孩子雙全時,刪除操作又該如何實施呢?

繼續上例,設擬再刪除二度節點36。如圖(b)(b)所示,首先找到該節點的直接後繼(40)。然後,只需如圖(c)(c)所示交換二者的數據項,則可將後繼節點等效地視作待刪除的目標節點。不難驗證,該後繼節點必無左孩子,從而相當於轉化爲此前相對簡單的情況。於是最後可如圖(d)(d)所示,將新的目標節點(36)替換爲其右孩子(46)。

請注意,在中途互換數據項之後,這一局部如圖(c)(c)所示曾經一度並不滿足順序性。但這並不要緊——不難驗證,在按照上述方法完成整個刪除操作之後,全樹的順序性必然又將恢復。

同樣地,除了更新全樹規模記錄和釋放被摘除節點,此時也要更新一系列祖先節點的高度。
不難驗證,此時首個需要更新高度的祖先(53),依然恰好由_hot指示。

在這裏插入圖片描述
在這裏插入圖片描述

平衡二叉樹

既然二叉搜索樹的性能主要取決於高度,節點數目固定的前提下,應儘可能地降低高度。

相應地,應儘可能地使兄弟子樹的高度彼此接近,即全樹儘可能地平衡。當然,包含n個節點的二叉樹,高度不可能小於log2nlog _2n若樹高恰好爲log2nlog_2n,則稱作理想平衡樹

在漸進意義下適當放鬆標準之後的平衡性,稱作適度平衡。

若將樹高限制爲“漸進地不超過O(logn)O(logn)”,則AVL樹、伸展樹、紅黑樹、kd-樹等,都屬於適度平衡。這些變種,因此也都可歸入平衡二叉搜索樹(balanced binary search tree,BBST)之列。

等價二叉搜索樹

若兩棵二叉搜索樹的中序遍歷序列相同,則稱它們彼此等價;反之亦然。

在這裏插入圖片描述
由該圖也不難看出,雖然等價二叉搜索樹中各節點的垂直高度可能有所不同,但水平次序完全一致。這一特點可概括爲“上下可變,左右不亂”

平衡二叉搜索樹的適度平衡性,都是通過對樹中每一局部增加某種限制條件來保證的。比如,在紅黑樹中,從樹根到葉節點的通路,總是包含一樣多的黑節點;在AVL樹中,兄弟節點的高度相差不過1。事實上,這些限制條件設定得非常精妙,除了適度平衡性,還具有如下局部性:

  1. 經過單次動態修改操作後,至多隻有O(1)O(1)處局部不再滿足限制條件
  2. 總可在O(logn)O(logn)時間內,使這O(1)O(1)處局部(以至全樹)重新滿足限制條件

這就意味着:
剛剛失去平衡的二叉搜索樹,必然可以迅速轉換爲一棵等價的平衡二叉搜索樹

等價二叉搜索樹之間的上述轉換過程,也稱作等價變換。

最基本的修復失衡二叉搜索樹手段,就是通過圍繞特定節點的旋轉,實現等價前提下的局部拓撲調整

zig和zag

在這裏插入圖片描述
如上圖(a)(a)所示,設ccZZvv的左孩子、右子樹,XXYYcc的左、右子樹。所謂以vv爲軸的zig旋轉,即如上圖(b)(b)所示,重新調整這兩個節點與三棵子樹的聯接關係:
XXvv作爲cc的左子樹、右孩子,YYZZ分別作爲vv的左、右子樹。

可見,儘管局部結構以及子樹根均有變化,中序遍歷序列仍是...,X,c,Y,v,Z,...{ ..., X, c, Y, v, Z, ... },故zig旋轉屬於等價變換。
在這裏插入圖片描述
對稱地如上圖(a)(a)所示,設XXccvv的左子樹、右孩子,YYZZ分別是cc的左、右子樹。所謂以vv爲軸的zag旋轉,即如上圖(b)(b)所示,重新調整這兩個節點與三棵子樹的聯接關係:
vvZZ作爲cc的左孩子、右子樹,XXYY分別作爲vv的左、右子樹。

同樣地,旋轉之後中序遍歷序列依然不變,故zag旋轉亦屬等價變換。

zig和zag旋轉均屬局部操作,僅涉及常數個節點及其之間的聯接關係,故均可在常數時間內完成。正因如此,在實現各種二叉搜索樹平衡化算法時,它們都是支撐性的基本操作。

就與樹相關的指標而言,經一次zig或zag旋轉之後,節點vv的深度加一,節點cc的深度減一;這一局部子樹(乃至全樹)的高度可能發生變化,但上、下幅度均不超過一層

AVL樹

通過合理設定適度平衡的標準,並藉助以上等價變換,AVL樹(AVL tree)可以實現近乎理想的平衡。在漸進意義下,AVL樹可始終將其高度控制在O(logn)O(logn)以內,從而保證每次查找、插入或刪除操作,均可在O(logn)O(logn)的時間內完成。

任一節點vv的平衡因子(balance factor)定義爲“其左、右子樹的高度差”,即:
balFac(v)=height(lc(v))height(rc(v))balFac(v)=height(lc(v)) - height(rc(v))
請注意,空樹高度取1-1,單節點子樹(葉節點)高度取00,與以上定義沒有衝突。

所謂AVL樹,即平衡因子受限的二叉搜索樹——其中各節點平衡因子的絕對值均不超過1

在完全二叉樹中各節點的平衡因子非0即1,故完全二叉樹必是AVL樹

失衡與重平衡

AVL樹與常規的二叉搜索樹一樣,也應支持插入、刪除等動態修改操作。但經過這類操作之後,節點的高度可能發生變化,以致於不再滿足AVL樹的條件。
在這裏插入圖片描述
以插入操作爲例,考查上圖(b)(b)中的AVL樹,其中的關鍵碼爲字符類型。現插入關鍵碼M'M',於是如圖(c)(c)所示,節點N'N'R'R'G'G'都將失衡。類似地,摘除關鍵碼Y'Y'之後,也會如圖(a)(a)所示導致節點R'R'的失衡。

如此因節點xx的插入或刪除而暫時失衡的節點,構成失衡節點集,記作UT(x)UT(x)。請注意,xx爲被摘除的節點,則UT(x)UT(x)僅含單個節點;但若xx爲被引入的節點,則UT(x)UT(x)可能包含多個節點

節點插入

不難看出,新引入節點xx後,UT(x)UT(x)中的節點都是xx的祖先,且高度不低於xx的祖父。以下,將其中的最深者記作g(x)g(x)。在xxg(x)g(x)之間的通路上,設ppg(x)g(x)的孩子,vvpp的孩子。注意,既然g(x)g(x)不低於xx的祖父,則pp必是xx的真祖先。

首先,需要找到如上定義的g(x)g(x)。爲此,可從xx出發沿parentparent指針逐層上行並覈對平衡因子,首次遇到的失衡祖先即爲g(x)g(x)。既然原樹是平衡的,故這一過程只需O(logn)O(logn)時間。

根據節點g(x)g(x)ppvv之間具體的聯接方向,將採用不同的局部調整方案

在這裏插入圖片描述
如上圖(a)(a)所示,設vvpp的右孩子,且ppgg的右孩子。這種情況下,必是由於在子樹vv中剛插入某節點xx,而使g(x)g(x)不再平衡。圖中以虛線聯接的每一對灰色方塊中,其一對應於節點xx,另一爲空。
此時,可做逆時針旋轉zag(g(x)g(x)),得到如圖(b)(b)所示的另一棵等價二叉搜索樹。
可見,經如此調整之後,g(x)g(x)必將恢復平衡。不難驗證,通過zig(g(x)g(x))可以處理對稱的失衡。

在這裏插入圖片描述
如上圖(a)(a)所示,設節點vvpp的左孩子,而ppg(x)g(x)的右孩子。這種情況,也必是由於在子樹vv中插入了新節點xx,而致使g(x)g(x)不再平衡。同樣地,在圖中以虛線聯接的每一對灰色方塊中,其一對應於新節點xx,另一爲空。
此時,可先做順時針旋轉zig(pp),得到如圖(b)(b)所示的一棵等價二叉搜索樹。再做逆時針旋轉zag(g(x)g(x)),得到如圖(c)(c)所示的另一棵等價二叉搜索樹。
此類分別以父子節點爲軸、方向互逆的連續兩次旋轉,合稱“雙旋調整”。可見,經如此調整之後,g(x)g(x)亦必將重新平衡。不難驗證,通過zag(pp)和zig(g(x)g(x))可以處理對稱的情況。

無論單旋或雙旋,經局部調整之後,不僅g(x)g(x)能夠重獲平衡,而且局部子樹的高度也必將復原。這就意味着,g(x)g(x)以上所有祖先的平衡因子亦將統一地復原——換而言之,在AVL樹中插入新節點後,僅需不超過兩次旋轉,即可使整樹恢復平衡

AVL樹的節點插入操作可以在O(logn)O(logn)時間內完成

節點刪除

與插入操作十分不同,在摘除節點xx後,以及隨後的調整過程中,失衡節點集UT(x)UT(x)始終至多隻含一個節點。而且若該節點g(x)g(x)存在,其高度必與失衡前相同
另外還有一點重要的差異是,g(x)g(x)有可能就是xx的父親

與插入操作同理,從_hot節點出發沿parentparent指針上行,經過O(logn)O(logn)時間即可確定g(x)g(x)位置作爲失衡節點的g(x)g(x),在不包含xx的一側,必有一個非空孩子pp,且pp的高度至少爲1。於是,可按以下規則從pp的兩個孩子(其一可能爲空)中選出節點vv
若兩個孩子不等高,則vv取作其中的更高者;否則,優先取vvpp同向者(亦即,vvpp同爲左孩子,或者同爲右孩子)

以下不妨假定失衡後g(x)g(x)的平衡因子爲+2(爲-2的情況完全對稱)。根據祖孫三代節點g(x)g(x)ppvv的位置關係,通過以g(x)g(x)pp爲軸的適當旋轉,同樣可以使得這一局部恢復平衡。

在這裏插入圖片描述
如上圖(a)(a)所示,由於在T3T3中刪除了節點而致使g(x)g(x)不再平衡,pp的平衡因子非負時,通過以g(x)g(x)爲軸順時針旋轉一次即可恢復局部的平衡。平衡後的局部子樹如圖(b)(b)所示。
同樣地這裏約定,圖中以虛線聯接的灰色方塊所對應的節點,不能同時爲空;T2T2底部的灰色方塊所對應的節點,可能爲空,也可能非空。

在這裏插入圖片描述
如上圖(a)(a)所示,g(x)g(x)失衡時若pp的平衡因子爲-1,則經過以pp爲軸的一次逆時針旋轉之後(圖(b)(b)),接着再以g(x)g(x)爲軸順時針旋轉,即可恢復局部平衡(圖(c)(c))。

對於刪除節點操作,g(x)g(x)恢復平衡之後,局部子樹的高度可能降低。對於插入節點操作,重平衡後不僅能恢復子樹的平衡性,也同時能恢復子樹的高度

與插入操作不同,在刪除節點之後,儘管也可通過單旋或雙旋調整使局部子樹恢復平衡,但就全局而言,依然可能再次失衡。對於刪除節點操作,若g(x)g(x)原本屬於某一更高祖先的更短分支,則因爲該分支現在又進一步縮短,從而會致使該祖先失衡。在摘除節點之後的調整過程中,這種由於低層失衡節點的重平衡而致使其更高層祖先失衡的現象,稱作“失衡傳播”

失衡傳播的方向必然自底而上,而不致於影響到後代節點。在此過程中的任一時刻,至多隻有一個失衡的節點;高層的某一節點由平衡轉爲失衡,只可能發生在下層失衡節點恢復平衡之後。因此,可沿parentparent指針逐層遍歷所有祖先,每找到一個失衡的祖先節點,即可套用以上方法使之恢復平衡。

統一重平衡算法——“3 + 4”重構

在這裏插入圖片描述
無論對於插入或刪除操作,從剛發生修改的位置xx出發逆行而上,直至遇到最低的失衡節點g(x)g(x)。於是在g(x)g(x)更高一側的子樹內,其孩子節點pp和孫子節點vv必然存在,而且這一局部必然可以g(x)g(x)ppvv爲界,分解爲四棵子樹——將它們按中序遍歷次序重命名爲T0T0T3T3

若同樣按照中序遍歷次序,重新排列g(x)g(x)ppvv,並將其命名爲aabbcc,則這一局部的中序遍歷序列應爲:
{T0,a,T1,b,T2,c,T3T0 , a, T1 , b, T2 , c, T3}

將這三個節點與四棵子樹重新如上“組裝”起來,恰好即是一棵AVL樹

結語

如果您有修改意見或問題,歡迎留言或者通過郵箱和我聯繫。
手打很辛苦,如果我的文章對您有幫助,轉載請註明出處。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章