1. 程式人生 > >《演算法(第四版)》排序-----優先佇列

《演算法(第四版)》排序-----優先佇列

  在實際應用中,我們常常不一定要求整個陣列全部有序,或者不需要一次就將它們排序,可能只需要當前陣列的鍵值最大的元素或最小的元素,這時就類似於總在處理下一個優先順序最高的元素,在這種情況下一個合適的資料結構應該支援兩種操作:刪除最大元素插入元素。這種型別叫做優先佇列

實現這種資料結構的方法有兩種,一種是簡單的陣列或連結串列來實現,一種是具有高效的二叉堆來實現,在本文中我們就是採用二叉堆來實現的。

  本文的目的是寫一個實現二叉堆的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]下取整的結點。