優先隊列是容許至少下列兩種操做的數據結構:insert 以及 deleteMin(找出、返回並刪除優先隊列中最小的元素)。node
insert 操做等價於 enqueue(入隊),而 deleteMin 則是運算 dequeue(出隊)在優先隊列中的等價操做。算法
可使用簡單鏈表進行不排序的插入,則插入操做爲 O(1),可是刪除須要遍歷鏈表爲 O(N)。
另外一種方法是讓鏈表保持排序狀態:插入代價高昂 O(N),但刪除爲 O(1).可是 deleteMin 的操做比插入操做少,前者可能更好。數組
另一種方法是使用二叉查找樹,它對這兩種操做的平均運行時間都爲 O(log N)。
可是,因爲咱們刪除的惟一元素是最小元,反覆出去左子樹的節點會損害樹的平衡使得右子樹加劇,在最壞狀況下 deleteMin 將左子樹刪空。數據結構
另外,使用查找樹有不少咱們數據結構不須要的鏈。eclipse
咱們將要使用的數據結構叫作二叉堆(binary heap)
,它的使用對於優先隊列的實現至關廣泛,以致於堆(heap)這個詞不加修飾的用在優先隊列的上下文中時,通常都是指數據結構的這種實現。函數
二叉堆有兩個性質:結構性和堆序性。ui
堆是一棵被徹底填滿的二叉樹,有可能的例外是在底層,底層上的元素從左到右填入
。這樣的樹被稱爲徹底二叉樹(complete binary tree)。this
一棵高爲 h 的徹底二叉樹有 2h 到 2h+1 - 1 個節點。它的高度爲 Log N,顯然它是 O(log N)。spa
由於二叉堆是滿二叉樹,因此在高度爲 h-1 的樹包含
20 + ... + 2h-1 = 2h -1 個元素,
在高度爲 h 的層上有 1 至 2h 個元素,因此應該有 2h 至 2h+1 - 1 個元素。設計
由於徹底二叉樹這麼有規律,因此它能夠用一個數組表示而不須要使用鏈。
對於數組中任意位置 i 上的元素,其左兒子在位置 2i 上,右兒子在左兒子後的單元 (2i + 1)上。它的父親則在位置 [i / 2] 上。
讓操做快速執行的性質是堆序性質(heap-order property)
。因爲咱們想要快速找出最小元,所以最小的元應該在根上。若是咱們考慮任意子樹也應該是一個堆,那麼任意節點就給應該小於它的全部後裔。
爲了將一個元素 X 插入到堆中,咱們在下一個可用位置建立一個空穴,不然該堆將不是徹底樹。若是 X 能夠放在該空穴中而並不破壞對的序,那麼插入完成。
不然,咱們把空穴的父節點移入該空穴中,這樣空穴就朝着根的方向上冒一步。繼續該過程直到 X 可以放入空穴中爲止。
以下圖所示:爲了插入 14,咱們在堆的下一個可用位置上創建一個空穴,因爲將 14 插入空穴破壞了堆序性質,所以將 31 移入該空穴。
在圖中,將元素從作到右執行插入,因此下一個空穴的位置應該在 31 的右節點上
。
當刪除一個最小元是,要在根節點創建一個空穴。因爲如今少了一個元素,所以堆中最後一個元素 X 必須移動帶該堆的某個地方。<u>這是爲了知足二叉堆的結構性質 -- 它是一棵徹底二叉樹,空穴只能在最後一層的最後一個元素以後</u>
所以,咱們將空穴的兩個兒子中較小者移入空穴這樣就把空穴向下推了一層。
重複該步驟直到 X 能夠被放入空穴中。
例如,對於下面的例子中,咱們先刪除元素 13,這將在二叉堆的根節點上創建一個空穴。隨後往裏面插入數字 31.
在堆的實現中常常發生的錯誤是當堆中存在偶數個元素的時候,可能會遇到某個節點只有一個子節點的狀況(只會在最下層出現)。
package com.mosby.ch06; /** * @author dhy */ public class BinaryHeap <T extends Comparable<? super T>> { public BinaryHeap(){ this(DEFAULT_CAPACITY); } public BinaryHeap(int capacity){ currentSize = 0; array = new Comparable[capacity]; } /** * 向堆插入一個元素<br><br> * * <blockquote> * * 在這裏咱們的代碼使用了一個小技巧:咱們如今的目的是要將當前堆中的空穴(初始爲數組中最後一個元素以後) * 移動到一個知足將 X 插入該空穴後不影響堆的性質的位置。<br><br> * * 若是咱們每次都將當前空穴的位置和它的父元素交換,那麼對於一個元素上濾 d 層, * 那麼因爲交換而執行的賦值次數就是 3d。<br><br> * * 而這裏,咱們每次只是在知足條件時將父節點的值賦給了這個空穴而沒有將空穴的值上濾。<br> * 這樣上濾 d 層將只須要 d 次對空穴的賦值和一次最後將 X 插入的賦值。總共 d+1 次賦值。 * * </blockquote> * * @param x */ public void insert(T x){ //由於堆內部的數組實現的第一個元素是空 if(currentSize == array.length - 1){ enlargeArray(array.length * 2 + 1); } //當前空穴的位置在最後一個元素的後一位,同時插入空穴以後 currentSize 增長一。等同於下面的代碼 //int hole = currentSize + 1; //currentSize++; int hole = ++currentSize; for(; hole > 1 && x.compareTo((T) array[hole / 2]) < 0; hole /= 2){ array[hole] = array[hole / 2]; } array[hole] = x; } public T findMin(){ if(isEmpty()){ return null; } return (T) array[1]; } public T deleteMin(){ if(isEmpty()){ throw new RuntimeException("Under flow"); } T minItem = findMin(); array[1] = array[currentSize--]; percolateDown(1); return minItem; } public boolean isEmpty(){ return currentSize == 0; } public void makeEmpty(){ currentSize = 0; } private static final int DEFAULT_CAPACITY = 100; private int currentSize;//當前堆中元素個數 private Comparable<? super T>[] array;//堆內部的以數組的形式存放 /** * 對空穴進行下濾 * @param hole */ private void percolateDown(int hole){ //這裏 child 的初值不會影響程序的正確性,可是 eclipse 的編譯器有 bug, int child; 是沒法經過編譯的 //在 IDEA 下能夠直接 int child; int child = hole * 2; Comparable<? super T> tmp = array[hole]; /** * 這裏注意一點,hole * 2 <= currentSize,由於數組的第一個元素爲空<br> * 數組中的實際元素應該是 array[i] - array[currentSize] */ for(; hole * 2 <= currentSize; hole = child){ child = hole * 2; /** * 在下濾的過程當中,咱們每次將當前節點的兩個子節點中較小的那個子節點跟空穴交換<br> * * 可是這必需要考慮一個問題,在最下層的時候,可能會有某個節點只有一個子節點<br> * * 而在非最下層則不會有這個問題,由於二叉堆是一個徹底二叉樹。<br> * * 而根據二叉堆的插入性質(從左往右插入),那麼只有一個元素的節點,這個元素的子節點確定 * 就是二叉堆的最後一個節點。此時 hole == currentSize. */ if(child != currentSize && array[child + 1].compareTo((T) array[child]) < 0){ child++; } if(array[child].compareTo((T) tmp) < 0){ array[hole] = array[child]; }else{ break; } } array[hole] = tmp; } /** * 若是提供了經過數組初始化二叉堆的方式,那麼在傳入一個數組後調用該方法便可獲得一個二叉堆。 */ private void buildHeap(){ for(int i = currentSize / 2; i > 0; i--){ percolateDown(i); } } public void enlargeArray(int newSize){ Comparable[] newArray = new Comparable[newSize]; for(int i = 1; i <= currentSize; i++){ newArray[i] = array[i]; } array = newArray; } public int size(){ return currentSize; } }
設計一種堆結構像二叉堆那樣有效的支持合併操做(即以 O(N) 時間處理一個 merge)並且只使用一個數組彷佛很困難。緣由在於,合併彷佛須要把一個數組拷貝到另一個數組中去。
<u>正由於如此,全部支持有效合併的高級數據結構都須要使用鏈式數據結構</u>
左式堆 (leftist heap)
像二叉堆同樣具備結構性和有序性。左式堆也是二叉樹,左式堆和二叉堆的惟一區別是:左式堆不是理想平衡(perfectly balanced),而實際上趨向於很是不平衡。
咱們把任意節點 X 的零路徑長(null path length) npl(X) 定義爲從 X 到一個不具備兩個兒子的節點的最短路徑長。所以,具備 0 個或一個兒子的節點的 npl 爲 0,而 npl(null) = -1。
任意節點的零路徑長比它的全部兒子節點的零路徑長的最小值
大1.這個結論也適用於少於兩個兒子的節點,由於 null 的零路徑長是 -1.
左式堆的性質是:對於堆中的
每個節點 X
,左兒子的零路徑長大於等於右兒子的零路徑長。
實際上,對於左式堆的任意一個節點只能有三種狀況,有兩個子節點、沒有子節點、僅有一個節點且該節點爲左子節點。
也就是說,若是存在任意一個節點只有右節點,那麼這個堆必定不是左式堆。可是,若是一個節點每一個節點都知足上面的條件,它不必定是左式堆,還須要知足零路徑長的條件。
顯然,在上路中,左圖是一棵左式堆;而右圖則不是,由於右圖的根節點的左子節點的左子節點的零路徑長 == 0,而根節點的左子節點的右子節點的零路徑長 == 1.
這個性質使得它不是一棵理想平衡樹,由於它顯然偏重於使樹向左增長深度。
由於左式堆趨向於加深左路徑,因此右路徑應該短。事實上,沿左式堆的右路徑是該堆中的最短路徑。不然,就會存在某個節點 X 的左兒子的最短路徑長小於右兒子的最短路徑長。
node / \ node `node` / \ / \ node node `null` node
例如,對於上面這個樹,對於標記的 node 節點是不符合左式堆的,由於它的左子節點的零路徑長是 -1,而右子節點的零路徑長是 0.
左式堆的基本操做是合併。注意,插入只是合併的特殊性狀況。
3 | 6 | / \ | / \ | 10 8 | 12 7 | / \ / | / \ / \ | 21 14 17 | 18 24 37 18 | / / | / | 23 26 | 33 |
合併具備大的 root 的堆與具備較小的 root 的堆的右節點
函數棧最上層 | 6 | | / \ | 8 | 12 7 | / | / \ / \ | 17 | 18 24 37 18 | / | / | 26 | 33 |
函數棧的底層,該層等待上層函數的返回 3 | / | 10 | / \ | 21 14 | / | 23 |
遞歸的去進行 merge 操做
函數棧最上層 8 | 7 | / | / \ | 17 | 37 18 | / | | 26 | |
函數棧第二層 | 6 | | / | | 12 | | / \ | | 18 24 | | / | | 33 |
函數棧的底層,該層等待上層函數的返回 3 | / | 10 | / \ | 21 14 | / | 23 |
繼續進行遞歸 merge
函數棧最上層 8 | | / | | 17 | 18 | / | | 26 | |
函數棧第二層 | 7 | | / | | 37 | | | | |
函數棧第三層 | 6 | | / | | 12 | | / \ | | 18 24 | | / | | 33 |
函數棧的底層,該層等待上層函數的返回 3 | / | 10 | / \ | 21 14 | / | 23 |
繼續進行遞歸 merge
函數棧最上層,這個時候函數棧開始退出 null | 18 |
函數棧最二層 8 | | / | | 17 | | / | | 26 | |
函數棧第三層 | 7 | | / | | 37 | | | | |
函數棧第四層 | 6 | | / | | 12 | | / \ | | 18 24 | | / | | 33 |
函數棧的底層,該層等待上層函數的返回 3 | / | 10 | / \ | 21 14 | / | 23 |
函數棧開始退出
函數棧最上層,上層函數退出,同時必須更新 root 節點的 npl 8 | | / \ | | 17 18 | | / | | 26 | |
函數棧第二層 | 7 | | / | | 37 | | | | |
函數棧第三層 | 6 | | / | | 12 | | / \ | | 18 24 | | / | | 33 |
函數棧的底層,該層等待上層函數的返回 3 | / | 10 | / \ | 21 14 | / | 23 |
函數棧繼續退出,同時若是root左子樹的零路徑長小於右子樹的零路徑長則必須翻轉兩個子樹
函數棧最上層,上層函數退出,同時必須更新 root 節點的 npl 7 / \ 8 37 | | / \ | | 17 18 | | / | | 26 | |
函數棧第二層 | 6 | | / | | 12 | | / \ | | 18 24 | | / | | 33 |
函數棧的底層,該層等待上層函數的返回 3 | / | 10 | / \ | 21 14 | / | 23 |
最後獲得的結果爲 圖 6-24 所示。
遞歸的退出條件是
:
被 merge 的兩個左式堆中任意一個爲 null,則返回另外一個;
兩個左式堆中那麼具備較小 root 節點的左子節點爲 null 時,將具備較大 root 的節點做爲具備較小 root 的節點的左子節點,並返回具備較小 root 的幾點。這裏隱含了一個信息:當一個左式堆的左子節點爲 null 時,它的右子節點一定爲 null。由於若是右子節點不爲 null,那麼它就不知足左式堆的條件了。
若是這兩個堆中有一個爲空,那麼咱們能夠返回另一個堆。
不然合併他們:
首先,咱們遞歸的將具備大的 root 的堆與具備小的 root的堆的右子堆合併。咱們在遞歸算法中須要保證遞歸獲得的這棵樹是左式堆。
爲何這裏是合併較大 root 的堆和較小 root 的堆的右子堆呢?
由於,咱們合併出來的這個堆須要作爲原來那個堆的右子堆,而根據左式堆的性質,一個節點全部的子節點都必須大於該節點。
圖 6-23 獲得的不是左式堆。左式的性質在根處被破壞。
在咱們步驟 1. 中獲得的新的子樹是左式堆,而右子樹自己就是左式堆,因此這棵樹是否是知足左式堆,只要左子樹的零路徑長大於新的右子樹的零路徑長便可。
若是不知足,咱們只須要將左子樹和右子樹的節點交換並更新零路徑長就能夠了。
package com.mosby.ch06; /** * 左式堆:與普通二叉堆區別在於,左式堆不是一個徹底二叉樹,而且左式堆不是一個理想平衡二叉樹。 */ public class LeftistHeap <E extends Comparable<? super E>> { public LeftistHeap(){ root = null; } /** * 公有的 merge 方法將 anotherLeftistHeap 合併到控制堆中。 * 隨後 anotherLeftistHeap 變成了空的。 * 在第一趟,咱們經過合併兩個堆的右路徑創建一棵新的樹。爲此,以排序的方式安排 H<sub>1</sub> 和 H<sub>2</sub> * 右路徑上的節點,保持他們各自的左兒子不變。 * 第二趟構成堆,兒子的交換工做在左式堆性質被破壞的那些節點上進行。 * <br> * @param anotherLeftistHeap 被合併的左式樹 */ public void merge(LeftistHeap<E> anotherLeftistHeap){ if(this == null){ return ; } root = merge(root, this.root); anotherLeftistHeap.root = null; } /** * 向左式樹中插入新的元素 * <br> * @param x */ public void insert(E x){ root = merge(new Node<>(x), root); } /** * 尋找左式堆中最小的元素 * <br> * @return 左式堆最小元素 */ public E findMin(){ if(isEmpty()){ return null; } return root.theElement; } /** * 刪除左式堆中最小元素,並返回該元素 * <br> * @return 被刪除的元素 */ public E deleteMin(){ if(isEmpty()){ return null; } E minItem = root.theElement; root = merge(root.left, root.right); return minItem; } /** * 返回左式堆是否爲空 * <br> * @return */ public boolean isEmpty(){ return root == null; } /** * 將左式堆設置爲空堆 */ public void makeEmpty(){ root = null; } /** * 內部類用於表示左式堆的節點,相對於普通的二叉樹多了 npl(null path length)用於記錄空路徑長 * <br> * @param <E> 節點中的存儲的對象 */ private static class Node<E>{ Node(E theElement){ this(theElement, null, null); } Node(E theElement, Node<E> left, Node<E> right){ this.theElement = theElement; this.left = left; this.right = right; npl = 0; } E theElement; Node<E> left; Node<E> right; int npl; } private Node<E> root; /** * merge 方法被用於消除一些特殊情形並保證 H<sub>1</sub> 有較小的根。 * <br> * @param h1 * @param h2 * @return */ private Node<E> merge(Node<E> h1, Node<E> h2){ if(h1 == null){ return h2; } if(h2 == null){ return h1; } if(h1.theElement.compareTo(h2.theElement) < 0){ return merge1(h1, h2); }else{ return merge1(h2, h1); } } /** * merge1 執行實際的合併操做,而且在 merge1 的調用中,h<sub>1</sub> 小於 h<sub>2</sub> * <br> * @param h1 * @param h2 * @return */ private Node<E> merge1(Node<E> h1, Node<E> h2){ //根據左式堆的性質,若是 h1.left == null,那麼 h1.right == null 也成立 if(h1.left == null){ h1.left = h2; }else{ h1.right = merge(h1.right, h2); if(h1.left.npl < h1.right.npl){ swapChildren(h1); } h1.npl = h1.right.npl + 1; } return h1; } private void swapChildren(Node<E> t){ Node<E> tmp = t.left; t.right = t.left; t.left = tmp; } }