《演算法(第四版)》排序-----優先佇列
在實際應用中,我們常常不一定要求整個陣列全部有序,或者不需要一次就將它們排序,可能只需要當前陣列的鍵值最大的元素或最小的元素,這時就類似於總在處理下一個優先順序最高的元素,在這種情況下一個合適的資料結構應該支援兩種操作:刪除最大元素和插入元素。這種型別叫做優先佇列
實現這種資料結構的方法有兩種,一種是簡單的陣列或連結串列來實現,一種是具有高效的二叉堆來實現,在本文中我們就是採用二叉堆來實現的。
本文的目的是寫一個實現二叉堆的API,以便後續需要優先佇列的這種資料結構時使用。優先佇列最重要的操作就是刪除最大元素(或最小,其實差別就在於在less()函式比較方向改變一下就行)和插入元素。為了保證靈活性,我們採用泛型,將實現了Comparable介面的資料型別作為引數Key。
表:泛型 優先佇列的API
------------------------------------------------------------------------------------------------------------
public class MaxPQ<Key extends Comparable<Key>>
------------------------------------------------------------------------------------------------------------
MaxPQ 建立一個優先佇列
MaxPQ(int max) 建立一個初始容量為max的優先佇列
MaxPQ(Key[] a) 用a[]中的元素建立一個優先佇列
---------------------------------------------------------------------------------------------------------------------------------------
void Insert(Key v) 向優先佇列中插入一個元素
Key max() 返回最大元素
Key delMax() 刪除並返回最大元素
boolean isEmpty() 返回佇列是否為空
int size() 返回優先佇列中的元素個數
-------------------------------------------------------------------------------------------------------------
上面的為MaxPQ的API,與MaxPQ類似,我們也可以一個MinPQ的API,含有一個delMin()方法來刪除並返回佇列中鍵值最小的那個元素,只需要改變less()的方向就可以了。
1.堆的定義
在二叉堆的陣列中,每個元素都要保證大於等於另兩個特定位置的元素,也就是父節點一定要大於等於它的兩個子節點,此時成為堆有序,由此可以退出,根節點是對有序的二叉樹中的最大節點。
2.二叉堆表示方法
我們將二叉樹的節點按照層級順序放入陣列中,根節點在位置1,它的子節點在位置2和3,而子節點的子節點分別在位置4,5 6,7以此類推。簡單起見,下文的二叉堆我們簡稱為堆。在一個對中位置k的節點的父節點為[k/2](向下取整),而它的兩個子節點位置分別為2k, 2k+1,這樣在不適用指標情況下就在陣列中實現了二叉堆。
一顆大小為N的完全二叉樹的高度為[lgN] 下取整 (2 ^ h= N ,所以h=lgN)
3.堆的演算法
我們用長度為N+1的私有陣列pq[]來表示大小為N的對(N+1的原因是為了滿足二叉樹的結構關係,不會用到pq[0])堆元素放在pq[1]到pq[N]中。在排序演算法中我們只通過less()和exchange()來訪問元素,因為元素都在pq[]中,所以此時傳入的引數不像之前部落格的幾篇排序演算法中的less()和exch()傳入整個陣列,僅僅傳入索引,具體實現如下
private boolean less(int i, int j) {
return pq[i].compareTo(pq[j]) < 0;
}
private void exch(int i, int j) {
Key temp = pq[i];pq[i] = pq[j];pq[j]=temp;
}
(1)由下至上的堆有序化(上浮)
當插入一個元素時,我們一般先放到陣列的末尾,然後讓它一點點向上移動,直到移動到它的父節點比他大,這種操作叫上浮swim(),這樣一趟後,陣列重新恢復有序性
rivate void swim(int k) {
while(k <=1 && less(k/2, k)){ //不斷的向上移動,直到已經是最高父節點或他的父節點比他大
exch(k/2, k);
k = k/2;
}
}
(2)由下至上的堆有序化(下沉)
一般刪除最大鍵值的方式是將最大鍵值與陣列的末尾進行交換,然後把最大鍵值的指向null,可是由於陣列的末尾肯定不是最大的父節點,所以需要將它一點點向下移動,直到移動到它的子節點比它小為止,這種操作就叫下沉 sink(),這樣一趟後,陣列重新恢復有序性
private void sink(int k) {
while(k*2 <= N){ //有兩種跳出迴圈方式,一種是已經到底了,另一種就是下面的不在比他小
int j = k * 2;
if(j < N && less(j, j+1)) j++; //一般讓這個節點和他子節點的最大值比較,這樣減少交換次數,所以j++
if(!(less(k, j))) break; //此處就是不比他小時跳出
exch(k ,j);
k = j;
}
}
其實可以把元素的上浮和下沉理解為一個公司,當來了一個新人(insert)時,就先把他放到最底層,然後發現他比別人好,那就一點點給他升級,知道上升到他的上級比他還好為止,這就是上浮,而一個公司的大boss走了,此時由於他的下級有兩個人,不知道讓誰上去好,為了平衡整個管理體系,大boss出了一招,他和最末尾的員工換位置,最末尾的員工顯然勝任不了最高階的位置,所以他就一點點的向下移動,知道他的下級比他還差,這叫下沉。
整體的程式碼實現如下
public class MaxPQ<Key extends Comparable<Key>> {
private Key[] pq; //基於堆的完全二叉樹
private int N = 0; //儲存於pq[1...N]中,pq[0]沒有用
public MaxPQ(int maxN){
pq = (Key[]) new Comparable[maxN +1];
}
public boolean isEmpty(){
return N == 0;
}
public int size(){
return N;
}
public void insert(Key k){
pq[++N] = k; //插入一個數組,N就加一
swim(N); //上浮,恢復堆的有序性
}
private void swim(int k) {
while(k <=1 && less(k/2, k)){
exch(k/2, k);
k = k/2;
}
}
private void exch(int i, int j) {
Key temp = pq[i];pq[i] = pq[j];pq[j]=temp;
}
private boolean less(int i, int j) {
return pq[i].compareTo(pq[j]) < 0;
}
public Key delMax(){
Key max = pq[1]; //從根節點取到最大元素,最高父節點
exch(1, N--); //讓根節點和陣列的最後一個節點交換,同時將N自減一
pq[N+1] = null; //將最大元素指向空
sink(1); //下沉,恢復堆的有序性
return max;
}
private void sink(int k) {
while(k*2 <= N){
int j = k * 2;
if(j < N && less(j, j+1)) j++;
if(!(less(k, j))) break;
exch(k ,j);
k = j;
}
}
}
4優先佇列呼叫示例
問題:輸入N個字串,每個字串都對應著一個整數,你的任務就是從中找出最大的(或最小的)M個整數(及其相關聯的字串),在某些禪境中輸入的量可能非常巨大,甚至可以認為是無限的,解決這個方法如果先排序,然後在找,或者不斷的替換最大元素,這種比較的代價會非常高昂,所以此時可使用優先佇列。
表: 從N個輸入中找到最大的M個元素所需要的成本
-------------------------------------------------------------------------------------------
增長的數量級
示 例 ---------------------------------------------------------------------
時 間 空 間
-----------------------------------------------------------------------------------------------------------------
排序演算法的用例 NlogN N
呼叫簡單實現的優先佇列 NM M
呼叫基於堆實現的優先佇列 NlogM M
-----------------------------------------------------------------------------------------------------------------
呼叫的示例如下:
從命令列輸入一個整數M,從輸入流種獲得一系列字串,輸入流的每一行代表一個交易,這段程式碼呼叫了MinPQ並會列印數字最大的M行,當優先佇列的大小超過M時就刪掉其中最小的元素,處理完所有交易,優先佇列中方正以增煦排列的最大的M個交易。
5.多叉堆
基於用陣列表示的完全三叉樹構造堆並修改相應的程式碼並不難,對應陣列中1至N的N個元素,位置k的結點大於大於等於3k-1,3k,3k+1的結點,小於位於[(k+1)/3]下取整的結點。