優先隊列html
許多應用程序都須要處理有序的元素,但不必定要求它們所有有序,或是不必定要一次就將它們排序。不少狀況下是收集一些元素,處理當前鍵值最大的元素,而後再收集更多的元素,再處理當前鍵值最大的元素。這種狀況下,須要的數據結構支持兩種操做:刪除最大的元素和插入元素。這種數據結構類型叫優先隊列。算法
這裏,優先隊列基於二叉堆數據結構實現,用數組保存元素並按照必定條件排序,以實現對數級別的刪除和插入操做。數組
1.API緩存
優先隊列是一種抽象數據類型,它表示了一組值和對這些值的操做,抽象層使應用和實現隔離開來。數據結構
2.初級實現spa
1.無序數組實現3d
優先隊列的 insert 方法和下壓棧的 push 方法同樣。刪除最大元素時,遍歷數組找出最大元素,和邊界元素交換。指針
2.有序數組實現code
插入元素時,將較大的元素向右移一格(和插入排序同樣)。這樣刪除時,就能夠直接 pop。htm
使用連接也是同樣的邏輯。
這些實現總有一種操做須要線性級別的時間複雜度。使用二叉堆能夠保證操做在對數級別的時間完成。
3.堆的定義
數據結構二叉堆能夠很好地實現優先隊列地基本操做。在二叉堆數組中,每一個元素都要保證大於等於另兩個特定位置地元素。一樣,這兩個位置地元素又至少要大於等於數組中另外兩個元素,以此類推。用二叉樹表示:
當一棵二叉樹的每一個結點都大於等於它的兩個子節點時,它被成爲堆有序。從任意結點向上,都能獲得一列非遞減的元素;從任意結點向下,都能獲得一列非遞增的元素。根結點是堆有序的二叉樹中最大的結點。
二叉堆表示法
這裏使用徹底二叉樹表示:將二叉樹的結點按照層級順序(從上到下,從左往右)放入數組中,不使用數組的第一個位置(爲了方便計算),根結點在位置 1 ,它的子結點在位置 2 和 3,子結點的子結點分別在位置 4,5,6,7,一次類推。
在一個二叉堆中,位置 k 的結點的父節點位置在 k/2,而它的兩個子結點在 2k 和 2k + 1。能夠經過計算數組的索引而不是指針就能夠在樹中上下移動。
一棵大小爲 N 的徹底二叉樹的高度爲 lgN。
4.堆的算法
用長度爲 N+1 的私有數組 pq[ ] 表示一個大小爲 N 的堆。
堆在進行插入或刪除操做時,會打破堆的狀態,須要遍歷堆並按照要求將堆的狀態恢復。這個過程稱爲 堆的有序化。
堆的有序化分爲兩種狀況:當某個結點的優先級上升(或在堆底加入一個新的元素)時,須要由下至上恢復堆的順序;當某個結點的優先級降低(例如將根節點替換爲一個較小的元素),須要由上至下恢復堆的順序。
上浮(由下至上的堆的有序化)
當某個結點比它的父結點更大時,交換它和它的父節點,這個結點交換到它父節點的位置。但有可能比它如今的父節點大,須要繼續上浮,直到遇到比它大的父節點。(這裏不須要比較這個子結點和同級的另外一個子結點,由於另外一個子結點比它們的父結點小)
//上浮 private void Swim(int n) { while (n > 1 && Less(n / 2, n)) { Exch(n/2,n); n = n / 2; } }
下沉(由上至下的堆的有序化)
當某個結點 k 變得比它的兩個子結點(2k 和 2k+1)更小時,能夠經過將它和它的兩個子結點較大者交換來恢復堆有序。交換後在子結點處可能繼續打破堆有序,須要繼續重複下沉,直到它的子結點都比它小或到達底部。
//下沉 private void Sink(int k) { while (2 * k <= N) { int j = 2 * k; //取最大的子節點 if (j < N && Less(j, j + 1)) j++; //若是父節點不小子節點,退出循環 if (!Less(k,j)) break; //不然交換,繼續下沉 Exch(j,k); k = j; } }
知道了上浮和下沉的邏輯,就能夠很好理解在二叉堆中插入和刪除元素的邏輯。
插入元素:將新元素加到數組末尾,增長堆的大小並讓這個新元素上浮到合適的位置。
刪除最大元素:從數組頂端(即 pq[1])刪除最大元素,並將數組最後一個元素放到頂端,減小數組大小並讓這個元素下沉到合適位置。
public class MaxPriorityQueue { private IComparable[] pq; public int N; public MaxPriorityQueue(int maxN) { pq = new IComparable[maxN+1]; } public bool IsEmpty() { return N == 0; } public void Insert(IComparable value) { pq[++N] = value; Swim(N); } public IComparable DeleteMax() { IComparable max = pq[1]; Exch(1,N--); pq[N + 1] = null; Sink(1); return max; } //下沉 private void Sink(int k) { while (2 * k <= N) { int j = 2 * k; //取最大的子節點 if (j < N && Less(j, j + 1)) j++; //若是父節點不小子節點,退出循環 if (!Less(k,j)) break; //不然交換,繼續下沉 Exch(j,k); k = j; } } //上浮 private void Swim(int n) { while (n > 1 && Less(n / 2, n)) { Exch(n/2,n); n = n / 2; } } private void Exch(int i, int j) { IComparable temp = pq[i]; pq[i] = pq[j]; pq[j] = temp; } private bool Less(int i, int j) { return pq[i].CompareTo(pq[j]) < 0; } }
上述算法對優先隊列的實現可以保證插入和刪除最大元素這兩個操做的用時和隊列的大小成對數關係。這裏省略了動態調整數組大小的代碼,能夠參考下壓棧。
對於一個含有 N 個元素的基於堆的優先隊列,插入元素操做只須要不超過(lgN + 1)次比較,由於 N 可能不是 2 的冪。刪除最大元素的操做須要不超過 2lgN次比較(兩個子結點的比較和父結點與較大子節點的比較)。
對於須要大量混雜插入和刪除最大元素的操做,優先隊列很適合。
改進
1. 多叉堆
基於數組表示的徹底三叉樹:對於數組 1 至 N 的 N 個元素,位置 k 的結點大於等於位於 3k-1, 3k ,3k +1 的結點,小於等於位於 (k+1)/ 3 的結點。
2.調整數組大小
使用動態數組,能夠構造一個無需關注隊列大小的優先隊列。能夠參考下壓棧。
3.索引優先隊列
在許多應用程序中,容許客戶端引用優先級隊列中已經存在的項目是有意義的。一種簡單的方法是將惟一的整數索引與每一個項目相關聯。
堆排序
咱們能夠把任意優先隊列變成一種排序方法:先將全部元素插入一個查找最小元素的優先隊列,再重複調用刪除操做刪除最小元素來將它們按順序刪除。這種排序成爲堆排序。
堆排序的第一步是堆的構造,第二步是下沉排序階段。
1.堆的構造
簡單的方法是利用前面優先隊列插入元素的方法,從左到右遍歷數組調用 Swim 方法(由上算法所需時間和 N logN 成正比)。一個更聰明高效的方法是,從右(中間位置)到左調用 Sink 方法,只需遍歷一半數組,由於另外一半是大小爲 1 的堆。這種方法只需少於 2N 次比較和 少於 N 次交換。(堆的構造過程當中處理的堆都比較小。例如,要構造一個 127 個元素的數組,須要處理 32 個大小爲 3 的堆, 16 個大小爲 7 的堆,8 個大小爲 15 的堆, 4 個大小爲 31 的堆, 2 個大小爲 63 的堆和 1 個大小爲127的堆,所以在最壞狀況下,須要 32*1 + 16*2 + 8*3 + 4*4 + 2*5 + 1*6 = 120 次交換,以及兩倍的比較)。
2.下沉排序
堆排序的主要工做在第二階段。將堆中最大元素和堆底元素交換,並下沉至 N--。至關於刪除最大元素並將堆底元素放至堆頂(優先隊列刪除操做),將刪除的最大元素放入空出的數組位置。
public class MaxPriorityQueueSort { public static void Sort(IComparable[] pq) { int n = pq.Length; for (var k = n / 2; k >= 1; k--) { Sink(pq, k, n); } //上浮須要遍歷所有 //for (var k = n; k >= 1; k--) //{ // Swim(pq, k); //} while (n > 1) { Exch(pq,1,n--); Sink(pq,1,n); } } private static void Swim(IComparable[] pq, int n) { while (n > 1 && Less(pq,n / 2, n)) { Exch(pq,n / 2, n); n = n / 2; } } //下沉 private static void Sink(IComparable[] pq,int k, int N) { while (2 * k <= N) { int j = 2 * k; //取最大的子節點 if (j < N && Less(pq,j, j + 1)) j++; //若是父節點不小子節點,退出循環 if (!Less(pq, k,j)) break; //不然交換,繼續下沉 Exch(pq, j,k); k = j; } } private static void Exch(IComparable[] pq, int i, int j) { IComparable temp = pq[i-1]; pq[i - 1] = pq[j - 1]; pq[j - 1] = temp; } private static bool Less(IComparable[] pq, int i, int j) { return pq[i - 1].CompareTo(pq[j - 1]) < 0; } public static void Show(IComparable[] a) { for (var i = 0; i < a.Length; i++) Console.WriteLine(a[i]); } }
堆排序的軌跡
將 N 個元素排序,堆排序只需少於 (2N lgN + 2N)次比較以及一半次數的交換。2N 來自堆的構造,2N lgN 是每次下沉操做最多須要 2lgN 次比較。
先下沉後上浮
在排序過程當中,大多數從新插入堆中的項目都會一直到達底部。所以,經過避免檢查元素是否已到達其位置,能夠簡單地提高兩個子結點中的較大者直到到達底部,而後上浮到適當位置,從而節省時間。這個方法將比較數減小了2倍,但須要額外的簿空間。只有當比較操做代價較高時可使用這種方法。(例如將字符串或其餘鍵值較長類型的元素排序)。
堆排序是可以同時最優利用空間和時間的方法,在最壞狀況下也能保證 ~2N lgN 次比較和恆定的額外空間。當空間緊張時,可使用堆排序。但堆排序沒法利用緩存。由於它的數組元素不多喝相鄰的其餘元素比較,所以緩存未命中的次數要遠高於大多數比較都在相鄰元素之間進行的算法。