這是數據結構類重新復習筆記的第 五 篇,同專題的其他文章可以移步:http://www.lxweimin.com/nb/39256701
堆(Heap)
堆(Heap)又稱為優先隊列(priority queue),在隊列的基礎上,堆允許所有隊列中的元素不一定按照先進先出(FIFO)的規則進行,而是使得每個元素有一定的優先級,優先級高的先出隊列。
這類數據結構屬于計算機科學中最雅致的一種
優先隊列至少存在兩個重要的操作:
-
insert
:插入,使得數據入隊列,是一種有約束的enqueue操作 -
deleteMin
:刪除最小項,找出、返回并刪除優先隊列中最小的元素,也就是最大優先級的元素,是一種有約束的dequeue操作
堆的簡單實現
有幾種簡單而明顯的方法實現優先隊列。
- 簡單鏈表:在表頭以O(1)執行插入操作,遍歷該鏈表以O(N)刪除最小元
- 始終有序的鏈表:插入代價以O(N)進行,刪除操作以O(1)進行
- 二叉查找樹:對插入和刪除兩種操作的平均運行時間都是O(log)。盡管插入是隨機的,而刪除則不是,但是并不影響這個結論,反復除去左子樹中的結點(最小值)損害了樹的平衡,使得右子樹加重。在最壞情形下,即將左子樹刪空的情形下,右子樹擁有的元素最多也就是它應具有的兩倍,這只是在其期望的深度上加了一個小常數
- ALV樹:可能有些過分,因為它支持大量并不需要的操作
二叉堆
二叉堆(binary heap)是一種對于優先隊列的實現,可以簡稱為堆
結構性質
堆是一棵完全二叉樹(complete binary tree),即所有節點都必須有左右兩個子節點,除了最后一排元素從左向右填入,直到沒有元素為止。
很顯然,一棵高為h的完全二叉樹有 2^h 到 2^(h+1)-1 個節點,即其高度為 logN 向下取整。
完全二叉樹的好處在于其規律性,可以使用一個數組而不需要鏈表來表示
對于數組中任一位置 i 上的元素,其左兒子在位置 2i 上,右兒子在左兒子后的單元 (2i+1) 上,它的父親則在位置 i/2 向下取整上。
因此,不僅不需要鏈,而且遍歷該樹所需要的操作也極簡單,在大部分計算機上都可能運行得非常快。唯一問題是最大的堆的大小需要事先估計。
堆序性質
使操作可以快速執行的性質是堆序性質(heap-order property):對于每一個節點X,X的父節點中的鍵小于等于X中的鍵,除沒有父節點根節點外。
基本操作
insert插入
將待插入的元素首先放置在最后一個位置上,以保證他是一個完全二叉樹,然后將該元素與其父節點(i/2向下取整)比較,如果比其父節點小,就將兩者互換,互換后再和新的父節點比較,這種方式稱為上濾(percolate up),得到一個小頂堆(min heap),如果比較的時候是較大的值向上走,就會得到一個大頂堆(max heap)
比如向一個小頂堆中插入元素14的操作:
deleteMin刪除
找出、返回并刪除最小元非常簡單,最小元就是根節點處的元素,將其返回并刪除。接下來是處理這個B。首先拿下最后一個元素X,如果元素X比B的兩個子節點都小,可以直接將X插入到B的位置,如果X比B的兩個子節點中的任意一個大,就不能插入,此時找到兩個子節點中較小的那個放到B處,B轉而移至這個子結點處。重復如上的步驟直到X可以插入B處為止。這個操作成為下濾(percolate down)
比如從一個小頂堆中刪除根節點
decreaseKey
decreaseKey(p, A)
操作減小在位置p處的元素的值,減少量為A,可以理解為調高了某個元素的優先級。操作破壞了堆的性質,從而需要上濾操作進行堆的調整。
increaseKey
increaseKey(p, A)
操作增加在位置p處的元素的值,增加量為A,可以理解為降低了某個元素的優先級。操作破壞了堆的性質,從而需要下濾操作進行堆的調整。
remove
remove(p)
操作刪除在堆中位置p處的節點,這種操作可以通過連續執行 decreaseKey(p, ∞)
和 deleteMin()
完成,可以理解馬上刪除某個一般優先級的元素
buildHeap
即將一個原始集合構建成二叉堆,這個構造過程即進行N次連續的insert
操作完成
定理:包含 2^(h+1)-1 個節點且高度為h的理想二叉樹(perfect binary tree)的節點的高度和為 2^(h+1)-1-(h+1)
d堆
d堆(d-Heaps)是二叉堆的簡單推廣,它與二叉堆很像,但是每個節點都有d個子節點,所以二叉堆是d為2的d堆。d堆是完全d叉樹。比如下邊的一個3堆。
d堆比二叉堆淺很多,其insert的運行時間改進到 O(logdN) 。但是deleteMin操作比較費時,因為要在d個子節點中找到最小的一個,需要進行d-1次比較。d堆無法進行find操作,而且將兩個堆合二為一是很困難的事情,這個附加操作為merge合并。
注意!在尋找節點的父節點、子節點的時候,乘法和除法都有因子d。如果d是一個2的冪,則可以通過使用二進制的移位操作計算,這在計算機中是非常省時間的。但是如果d不是一個2的冪,則使用一般的乘除法計算,時間開銷會急劇增加。有證據顯示,實踐中,堆可以勝過二叉堆
左式堆
這些高級的數據結構很難使用一個數據結構來實現,所以一般都要用到鏈式數據結構,這種結構可能會使得其操作變慢。
左式堆的定義
零路徑長(null path length)npl(X):定義為從一個X節點到其不具有兩個子節點的子節點的最短路徑長,即具有0個或者1個子節點的節點npl=0,npl(null)=-1,任意節點的零路徑長都比其各個子節點中零路徑長最小值多1。
左式堆(leftist heap)是指對于任意一個節點X,其左子節點的零路徑長都大于等于其右子節點的零路徑長。很顯然,左式堆趨向于加深左路徑。比如下邊的兩個堆,只有左邊的是左式堆,堆的節點標示的是該節點的零路徑長。
左式堆的實現中,需要有四個值:數據、左指針、右指針和零路徑長。
struct LeftistHeap
{
Object data;
LeftistHeap* leftChild;
LeftishHeap* rightChild;
int npl;
}
定理:在右路徑上有r個節點的左式堆必然至少有 2^r-1 個節點
左式堆的合并
merge
是左式堆的基本操作,insert
插入可以看成是一個單節點的堆與一個大堆的merge
,deleteMin
刪除最小值操作可以看成是首先返回、刪除根節點,然后將根節點的左右子樹進行merge
。所以merge
是左式堆的基本操作。
假設現在有兩個非空的左式堆H1和H2,merge操作遞歸地進行如下的步驟:
- 將H1和H2中根節點較的堆(比如是H2)與根節點較小的堆(比如是H1)的右子堆進行merge
- 讓新的merge的堆成為根節點較小的堆(H1)的右字堆
- 如果在根結點處出現左右子堆不符合左式堆的條件的情況,互換左右子堆的位置并更新npl的值
例如如下的兩個堆:
將H2與H1的右子樹(8--17--26)進行merge操作,此時(8--17--26)和H2的merge操作中又需要(8--17--26)和H2的右子堆(7--37--18)進行merge操作……如此遞歸得到如下的堆:
然后根據遞歸的最外層(回到H1和H2的merge的第二步),將上邊合并的堆成為H1的右子堆
此時根節點(3)處出現了左右子堆不符合左式堆的情況,互換左右子堆并更新零路徑長的值
斜堆
斜堆(skew heap)是左式堆的自調節形式,實現起來極其簡單。斜堆和左式堆的關系類似于伸展樹和AVL樹之間的關系。斜堆是具有堆序的二叉樹,但是不存在對樹的結構的現限制。不同于左式堆,關于任意結點的零路徑長的任何信息都不保留。斜堆的右路徑在任何時刻都可以任意長,因此,所有操作的最壞情形運行時間均為O(N)。然而,正如伸展樹一樣,可以證明對任意M次連續操作,總的最壞情形運行時間是 O(MlogN)。因此,斜堆每次操作的攤還開銷(amortized cost)為O(logN)
斜堆的合并
斜堆的基本操作也是merge合并,和左式堆的合并相同,但是不需要對不滿足左右子堆的左式堆條件的節點進行左右子堆的交換。斜堆的交換是無條件的,除右路徑上所有節點的最大者不交換它的左右兒子外,都要進行這種交換。
比如將上述的H1和H2進行merge合并操作
首先進行第一步,除了交換左右子樹的操作與左式堆不同,其他的操作都相同
將合并的堆作為H1的右子堆并交換左右子堆,得到合并后的斜堆
二項隊列
二項隊列(binomial queue)支持merge、insert和deleteMin三種操作,并且每次操作的最壞情形運行時間為O(logN),插入操作平均花費常數時間。
二項隊列的結構
二項隊列不是一棵堆序的樹,而是堆序的樹的集合,成為森林(forest)。堆序樹中的每一棵都是有約束的二項樹(binomial tree)。二項樹是每一個高度上至多存在一棵二項樹。高度為0的二項樹是一棵單節點樹,高度為k的二項樹Bk通過將一棵二項樹Bk-1附接到另一棵二項樹Bk-1的根上而構成的。如下圖的二項樹B0、B1、B2、B3和B4。
可以看到二項樹Bk由一個帶有兒子B0,B1,……,Bk-1的根組成。高度為k的二項樹恰好有2^k個節點,而在深度d處的節點數為二項系數Cdk。
我們可以使用二項樹的集合唯一地表示任意大小的優先隊列。以大小為13的隊列為例,13的二進制表示為1101,從而我們可以使用二項樹森林B3、B2、B0表示,即二進制表示的數中,第k位為1表示Bk樹出現,第k位為0表示Bk樹不出現。比如上述的堆H1和堆H2可以表示為如下的兩個二項隊列:
二項隊列的合并
二項隊列額merge合并操作非常簡單,以上邊的二項隊列H1、H2為例。需要將其合并成一個大小為13的隊列,即B3、B2、B0。
首先H2中有一個B0,H1中沒有,所以H2中的B0可以直接作為新的隊列的B0的樹
其次H1和H2中兩個B1的樹可以合并成一個新的B2的樹,只需要將其中根節點較小的堆掛到根節點較大的堆的根節點上。這樣就得到了三棵B2堆,將其中根節點最大的堆直接放到新隊列中成為它的B2堆。
最后將兩個B2堆合并成一個新隊列中的B3堆。
二項隊列的deleteMin很簡單,只需要比較隊列中所有二項堆的根節點,返回和刪除最小的值即可,時間復雜度為O(logN),然后進行一次merge操作,也可以使用一個單獨的空間每次記錄最小值,這樣就可以以O(1)的時間返回。
二項隊列的實現
森林中樹的實現采用“左子右兄弟”的表示方法,然后二項隊列可以使用一個數組來記錄森林中每個樹的根節點。
例如上邊的合成的二項隊列可以表示成如下的樣子:
標準庫中的優先隊列
STL中,二叉堆是通過priority_queue
模板類實現的,在頭文件queue
中,STL實現一個大頂堆而不是小頂堆,其關鍵的成員函數如下:
void push( const object & x );
const Object & top( ) const;
void pop( );
bool empty( );
void clear( );
轉載請注明出處,本文永久更新鏈接:https://blogs.littlegenius.xin/2019/08/24/【數據結構】五堆/