6. 1 模型
????????優先隊列是允許至少下列兩種操作的數據結構: insert(插入),它的作用是顯而易見的;以及deleteMin(刪除最小者), 它的工作是找出、返回并刪除優先隊列中最小的元素。insert操作等價于enqueue(入隊),而deleteMin則是隊列運算dequeue(出隊)在優先隊列中的等價操作。
????????如同大多數數據結構那樣,有時可能要添加一些其他的操作,但這些添加的操作屬于擴展的操作,而不是圖6-1所描述的基本模型的一部分。
6.2 一些簡單的實現
????????有幾種明顯的方法可用于實現優先隊列。我們可以使用一個簡單鏈表在表頭以0(1)執行插入操作,并遍歷該鏈表以刪除最小元,這又需要O(N)時間。另一種方法是始終讓鏈表保持排序狀態;這使得插入代價高昂(O(N)) 而deleteMin花費低廉(0(1))。基于deleteMin的操作從不多于插入操作的事實,前者恐怕是更好的想法。
????????另一種實現優先隊列的方法是使用二叉查找樹,它對這兩種操作的平均運行時間都是O(log N)。盡管插入是隨機的,而刪除則不是,但這個結論還是成立的。記住我們刪除的唯一元素是最小元。 反復除去左子樹中的節點似乎會損害樹的平衡,使得右子樹加重。 然而,右子樹是隨機的。 在最壞的情形下,即deleteMin將左子樹刪空的情形下,右子樹擁有的元素最多也就是它應具有的兩倍。 這只是在期望的深度上加了 個小常數。 注意,通過使用 棵平衡樹,可以把這個界變成最壞情形的界;這將防止出現壞的插入序列。
????????使用查找樹可能有些過分,因為它支持許許多多并不需要的操作。 我們將要使用的基本的數據結構不需要鏈,它以最壞情形時間0(logN)支持上述兩種操作。 插入操作實際上將花費常數平均時間,若尤刪除操作的干擾,該結構的實現將以線性時間建立 個具有N項的優先隊列。
6.3二叉堆
????????我們將要使用的這種工具叫作二叉堆(binary heap) , 它的使用對于優先隊列的實現相當普遍,以至于當堆(heap)這個詞不加修飾地用在優先隊列的上下文中時,一般都是指數據結構的這種實現。在本節,我們把二叉堆只叫作堆。像二叉查找樹一樣,堆也有兩個性質,即結構性和堆序性。恰似AVL樹,對堆的一次操作可能破壞這兩個性質中的一個,因此,堆的操作必須到堆的所有性質都被滿足時才能終止。事實上這并不難做到。
6.3. 1 結構性質
? ??????堆是一棵被完全填滿的二叉樹,有可能的例外是在底層,底層上的元素從左到右填入。這樣的樹稱為完全二叉樹(complete binarytree)。圖6-2給出了一個例子。
????????完全二叉樹的高是 log N,顯然它是0(logN)。
????????一個重要的觀察發現,因為完全二叉樹這么有規律,所以它可以用一個數組表示而不需要使用鏈。
????????對于數組中任一位置i上的元素,其左兒子在位置2i上,右兒子在左兒子后的單元(2i +1)中,它的父親則在位置[i/2]上。因此,這里不僅不需要鏈,而且遍歷該樹所需要的操作極簡單,在大部分計算機上運行很可能非常快。這種實現方法的唯一問題在于,最大的堆大小需要事先估計,但一般這并不成問題(而且如果需要, 我們可以重新調整大小)。在圖6-3中,堆大小的限界是13個元素。該數組有一個位置0, 后面將詳細敘述。
????????因此, 一個堆結構將由一個(Comparable對象的)數組和一個代表當前堆的大小的整數組成。圖6-4顯示一個優先隊列的架構。
????????本章我們將始終把堆畫成樹,這意味著具體的實現將使用簡單的數組。
6.3.2 堆序性質
????????讓操作快速執行的性質是堆序性質(heap-order property)。由于我們想要快速找出最小元,因此最小元應該在根上。如果我們考慮任意子樹也應該是一個堆,那么任意節點就應該小于他的所有后裔。
????????應用這個邏輯,我們得到堆序性質。在一個堆中,對于每一個節點X,X 的父親中的關鍵字小于(或等于)X 中的關鍵字,根節點除外(它沒有父親)。
https://gitee.com/sunyunjie/DataStrycturesAndAlgorithmAnalysis
6.3.3 基本的堆操作
insert(插入)
????????為將一個元素X插入到堆中,我們在下一個可用位置創建一個空穴,否則該堆將不是完全樹。如果X可以放在該空穴中而并不破壞堆的序,那么插入完成。 否則,我們把空穴的父節點上的元 素移入該空穴中,這樣,空穴就朝著根的方向上冒一步。 繼續該過程直到X能被放入空穴中為止。如圖6-6所示,為了插入14, 我們在堆的下一個可用位置建立一個空穴。 由于將14插入空穴破壞了堆序性質,因此將31移入該空穴。在圖6-7中繼續這種策略,直到找出置入14的正確位置。
????????如果欲插入的元素是新的最小元從而一直上濾到根處,那么這種插入的時間將長達O(logN)。 平均看來,上濾終止得要早;
? ??????deleteMin(刪除最小元)
? ??????deleteMin以類似于插入的方式處理。找出最小元是容易的,困難之處是刪除它。當刪除一個最小元時,要在根節點建立一個空穴。由于現在堆少了一個元素,因此堆中最后一個元素X 必須移動到該堆的某個地方。如果X可以被放到空穴中,那么dele七eMin完成。不過這一般不太可能,因此我們將空穴的兩個兒子中較小者移入空穴,這樣就把空穴向下推了一層。重復該步驟直到X可以被放入空穴中。因此,我們的做法是將X置入沿著從根開始包含最小兒子的一條路徑上的一個正確的位置。
????????圖6-9中左圖顯示了deleteMin之前的堆。刪除13后,我們必須試圖正確地將31放到堆中。31 不能放在空穴中,因為這將破壞堆序性質。于是,我們把較小的兒子14置入空穴,同時空穴下滑一層(見圖6-10)。重復該過程,山于31大于19, 因此把19置入空穴,在更下一層上建立一個新的空穴。然后,由于31還是太大, 因此再把26置入空穴,在底層又建立一個新的空穴。最后, 我們得以將31置入空穴中(圖6-11)。這種一般的策略叫作下濾(percolate down)。在其實現例程中我們使用類似于在insert例程中用過的技巧來避免進行交換操作。
????????這種操作最壞情形運行時間為 O(log N)。平均而言,被放到根處的元素幾乎下濾到堆的底層(即它所來自的那層),因此平均運行時間為 O(log N) 。
6. 5 d-堆
? ??????二叉堆是如此簡單,以至于它們幾乎總是用在需要優先隊列的時候。d-堆是二叉堆的簡單推廣,它就像一個二叉堆,只是所有的節點都有d個兒子(因此,二叉堆是2-堆)。
????????圖 6-19 表示的是一個 3-堆。注意,d-堆要比二叉堆淺得多,它將 insert操作的運行時間 改進為 O(logd N)。然而,對于大的 d, deleteMin 操作費時得多,因為雖然樹是淺了,但是 d個兒子中的最小者是必須要找出的,如使用標準的算法,這會花費d-1次比較,于是將操作的用時提高到 O(d logd N)。如果 d 是常數,那么當然兩個的運行時間都是 O(log N)。雖然仍然可以使用一個數組,但是,現在找出兒子和父親的乘法和除法都有個因子 d, 除非 d 是 2 的幕,否則將會大大增加運行時間,因為我們不能再通過移一個二進制位來實現除法了。d-堆在理論上很有趣,因為存在許多算法,其插入次數比 dele匡Min 的次數多得多(因此理論上的加速是可能的)。當優先隊列太大而不能完全裝入主存的時候,d-堆也是很有用的。在這種情況下,d-堆 能夠以與B樹大致相同的方式發揮作用。最后,有證據顯示,在實踐中4-堆可以勝過二叉堆。
6.6 左式堆
? ??????設計一種堆結構像二叉堆那樣有效地支持合并操作(即以o(N)時間處理一個merge)而且 只使用一個數組似乎很困難。 原因在于,合并似乎需要把一個數組拷貝到另一個數組中去,對于相同大小的堆這將花費時間O(N)。 正因為如此,所有支持有效合并的高級數據結構都需要使用鏈式數據結構。
6. 6. 1 左式堆性質
????????我們把任一節點X的零路徑長(null pathleng th) npl (X)定義為從X到一個不具有兩個兒子的節點的最短路徑的長。因此,具有0個或一個兒子的節點的npl 為o, 而npl(null)= -1。在圖6-20的樹中, 零路徑長標記在樹的節點內。
????????注意,任一節點的零路徑長比它的各個兒子節點的零路徑長的最小值大l。這個結論也適用少于兩個兒子的節點,因為null的零路徑長是-1。
????????左式堆性質是:對于堆中的每一個節點X, 左兒子的零路徑長至少與右兒子的零路徑長相等。圖6-20中只有一棵樹,即左邊的那棵樹滿足該性質。這個性質實際上超出了它確保樹不平衡的要求,因為它顯然偏重于使樹向左增加深度。確實有可能存在由左節點形成的長路徑構成的樹(而且實際上更便于合并操作) 因此,我們就有了名稱左式堆(leftist heap)。
????????因為左式堆趨向于加深左路徑,所以右路徑應該短。事實上,沿左式堆右側的右路徑確實是該堆中最短的路徑。否則,就會存在過某個節點X的一條路徑通過它的左兒子,此時X 就破壞了左式堆的性質。
定理6.2 在右路徑上有r個節點的左式樹必然至少有2^r-1個節點。
6.6.2 左式堆操作
????????對左式堆的基本操作是合并。 注意,插入只是合并的特殊情形,因為我們可以把插入看成 是單節點堆與一個大的堆的merge。 首先,我們給出一個簡單的遞歸解法,然后介紹如何能夠非遞歸地執行該解法。 我們的輸入是兩個左式堆H1和H2, 見圖6-21。 讀者應該驗證,這些堆確實是左式堆。 注意, (最小的元素在根處。 除數據、左引用和右引用所用空間外,每個節點還要有一個指示零路徑長的項。
????????如果這兩個堆中有 個堆是空的,那么我們可以返回另外一個堆。 否則,合并這兩個堆, 比較它們的根。 首先,我們遞歸地將具有大的根值的堆與具有小的根值的堆的右子堆合并,, 在本例中,我們遞歸地將H2與h1的根在8處的右子堆合并, (18得到圖6-22中的堆。
? ??????由于這棵樹是遞歸形成的,而我們尚未 對算法描述完畢,因此,現在還不能說明該是如何得到的。 不過,有理由假設,最后 的結果是一個左式堆,因為它是通過遞歸的步驟得到的。 這很像歸納法證明中的歸納 圖6-22 將H2 與凡的右子堆合并的結果假設。 既然我們能夠處理基準情形(發生在一棵樹是空的時候),當然可以假設,只要能夠完成合并那么遞歸步驟就是成立的,這是遞歸
6.7 斜堆
????????斜堆(skew heap)是左式堆的自調節形式,實現起來極其簡單。 斜堆和左式堆間的關系類似于伸展樹和AVL樹間的關系。 斜堆是具有堆序的二叉樹,但不存在對樹的結構限制。 不同于左 式堆,關于任意節點的零路徑長的任何信息都不再保留。 斜堆的右路徑在任何時刻都可以任意長,因此,所有操作的最壞情形運行時間均為O(N)。 然而,正如伸展樹一樣,可以證明(見第11章)對任意M次連續操作,總的最壞情形運行時間是O(Mlog N)。 因此,斜堆每次操作的攤還開銷 (amortized cost) 為 O(logN)。
? ??????與左式堆相同,斜堆的基本操作也是合并操作。merge例程還是遞歸的,我們執行與以前完全相同的操作,但有一個例外,即:對于左式堆,我們查看是否左兒子和右兒子滿足左式堆結構性質, 并在不滿足該性質時將它們交換。但對于斜堆,交換是無條件的,除那些右路徑上所有節點的最大者不交換它的左右兒子的例外外, 我們都要進行這種父換。這個例外就是在自然遞歸實現時所發生的情況,因此它實際上根本不是特殊情形。此外,證明時間界也是不必要的,但是,由于這樣的節點肯定沒有右兒子,因此執行交換是不明智的(在我們的例子中,該節點沒有兒子,因此我們不必為此擔心)。另外,仍設我們的輸入是與前面相同的兩個堆,見圖6-31如果我們遞歸地將H2與H1
的根在8處的子堆合并,那么將得到圖6-32中的堆。
?????????注意,因為右路徑可能很長,所以遞歸實現可能由于缺乏棧空間而失敗,盡管在其他方面性能是可接受的。 斜堆有一個優點,即不需要附加的空間保留路徑長以及不需要測試以確定何時交換兒子。 精確確定左式堆和斜堆的右路徑長的期望值是一 個尚未解決的問題(后者無疑更為困難)。 這樣的比較將更容易確定平衡信息的輕微遺失是否可由缺乏測試來補償。
6.8 二項隊列
????????雖然左式堆和斜堆都在每次操作以O(logN)時間有效地支持合并、插入和deleteMin,但還是有改進的余地,因為我們知道,二叉堆以每次操作花費常數平均時間支持插入。二項隊列支持所有這三種操作,每次操作的最壞情形運行時間為0(logN) , 而插入操作平均花費常數時間。
6. 8. 1 二項隊列結構
????????二項隊列(binomial queue)與我們已經看到的所有優先隊列的實現的區別在于,一個二項隊 列不是一棵堆序的樹,而是堆序的樹的集合,稱為森林(forest)。每一棵堆序樹都是 有約束的形式,叫作二項樹(binomial tree , 后面將看到該名稱的由來是顯然的)。每一個高度上至多存在一棵二項樹。高度為0的二項樹是一棵單節點樹;高度為k的二項樹凡通過將一棵二項樹 Bk -I附接到 另 一棵二項樹 Bk-I的根上而構成。圖6-34顯示二項樹 B0,B1,B2,B3以及B4.
????????從圖中看到,二項樹隊由一個帶有兒子B0, B1,。。。,BK-1的根組成。高度為k的二項樹恰好有2k個節點,而在深度d 處的節點數是二項系數(d)。如果我們把堆序施加到二項樹上并允許任意高度上最多一棵二項樹,那么就能夠用二項樹的集合表示任意大小的優先隊列。例如,大小為13 的優先隊列可以用森林B3, B2, B。表示。我們可以把這種表示寫成1101, 它不僅以二進制表示了13'而且也表示這樣的事實:在上述表示中,B3, B2, B。出現,而B1則沒有。