1. 程式人生 > 實用技巧 >排序-優先佇列(堆排序)

排序-優先佇列(堆排序)

優先佇列

很多時候我們可能需要處理有序的元素,但是並不需要全部有序,可能只需要取出某個優先順序較高的元素(最大優先佇列),只處理當前鍵值較大元素,API為插入元素刪除最大元素。這種資料型別為優先佇列
API:

public class MaxPQ<Key extends Comparable<Key>> {
    // 建立一個優先佇列
    public MaxPQ(){}
    // 建立一個初始容量為max的優先佇列
    public MaxPQ(int max){}
    // 用陣列a的元素建立一個優先佇列
    public MaxPQ(Key[] a){}
    // 向優先佇列插入一個元素
    public void insert(Key v){}
    // 返回最大元素
    public Key max(){return null;}
    // 刪除並返回最大元素
    public Key delMax(){return null;}
    // 佇列是否為空
    public boolean isEmpty(){return false;}
    // 優先佇列元素個數
    public int size(){return 0;}
}

或許我們可以將陣列排序後取出最值即可,那麼增長數量級是比較高的,並且我們並不需要整個陣列是有序的,使用堆結構進行處理是個不錯的選擇。

二叉堆

一組堆有序的完全二叉樹排序的元素(不使用第一個位置),堆有序指父節點大於等於兩個子節點。

實現策略

插入元素後將新元素與父節點做比較如果大於父節點則與父節點交換,然後在與父節點的父節點比較,直到小於父節點或者為根節點,整個過程稱為上浮。程式碼如下:

 // 元素上浮
    private void swim(int k){
        while (k>1&&less(k/2,k)){
            exch(k/2,k);//  父節點小於子節點交換
            k/=2;// 一直上浮
        }
    }

示意圖

基於二叉堆的完全二叉樹根節點即為最大元素,刪除元素只需將第一個元素取出即可(根節點),然後將第一個節點與最後一個結點交換,接著將第一個結點與兩個子結點作比較,當小於較大的子節點時交換,直到大於兩個子結點或者到達底部時結束。整個過程稱為下沉操作,程式碼如下:

 // 元素下沉
    private void sink(int k){
        while (2*k<N){
           int j = 2*k;
           if(j<N&&less(j,j+1))j++;// j小於j+1則較大的是j+1
           if(!less(k,j)) break;// 當前值大於最大的子節點不進行交換
           exch(j,k);
           k=j;
        }
    }

示意圖

插入和刪除實現如下:

 // 向優先佇列插入一個元素
    public void insert(Key v){
        pq[++N] = v;
        swim(N);// 新插入的結點上浮找到自己位置
    }
    // 返回最大元素
    public Key max(){return pq[1];}
    // 刪除並返回最大元素
    public Key delMax(){
        Key max = pq[1];
        exch(1,N--);// 將最後一個與第一個交換後下沉即可
        pq[N+1] =null;// 放置物件遊離
        sink(1);
        return max;
    }

分析

由樹狀圖可知,插入和刪除操作的執行時間取決於樹的高度,在歸併排序中有分析可知執行時間為對數級別的。插入操作最多不超過lgN+1次比較,刪除操作不超過2lgN次比較(下沉中有兩次比較)。如果使用陣列進行排序後取值,意味著我們需要線性級別的執行時間。而基於堆有序的完全二叉樹實現的優先佇列突破了這個限制。

改進

  1. 多叉堆
    可以構建二叉堆同樣可以構建多叉堆,假設為三叉堆,則位置k的結點大於3k-1、3k、3k+1結點的元素,構建樹高度為3n=N,增長數量級為log3N。根據歸納可知d叉堆的增長數量級為logdN
  2. 動態調整陣列大小
    插入元素調整陣列加倍,刪除時減小陣列為一半,無需關注佇列大小的限制

堆排序

public class Heap {
    private Heap(){}
    private static boolean less(Comparable[] a,int vIndex,int wIndex){
        return a[vIndex-1].compareTo(a[wIndex-1])<0;
    }
    private static void exch(Comparable[] a,int vIndex,int wIndex){
        Comparable temp = a[vIndex-1];
        a[vIndex-1] = a[wIndex-1];
        a[wIndex-1] = temp;
    }
    // 下沉操作
    private static void sink(Comparable[] a,int k,int N){
        while (2*k<=N){
            int j=2*k;
            if(j<N&&less(a,j,j+1))j++;
            if(!less(a,k,j))break;
            exch(a,k,j);
            k=j;
        }
    }
    public static void sort(Comparable[] a){
        int N =a.length;
        // 構建二叉堆,倒序下沉的話可以跳過單節點的情況
        for(int i=N/2;i>=1;i--){
            sink(a,i,N);
        }
        // 從小到大排序
        while (N>1){
            exch(a,1,N--);// 將最大值放在最後
            sink(a,1,N);// 交換過來的值找到自己的位置
        }
    }
}

由於為了與二叉堆一致的從1開始計數,所以less與exch對索引的操作都進行了-1

實現策略

基於二叉堆的排序,先將陣列按照下沉的方法一個一個構成最大優先佇列然後在將第一個(最大值)與最後一個交換後,將當前第一個值進行下沉找到自己位置,此時又是堆有序的完全二叉樹,繼續與倒數第二個交換,最終為從小到大的排序。

分析

堆排序是基於二叉堆的排序,是一種非常優雅的排序方法。先對陣列進行二叉堆的構造,然後將最大值依次放在最後。在進行二叉堆構造時可以使用一個一個上浮的方法,但是在使用的時候我們使用下沉的方法,下沉我們只需對1/2的元素進行下沉操作,最後的單節點元素我們不需要進行下沉操作。用下沉操作進行二叉堆的構造只需最多2N次比較和N次交換。證明如下:

假設高度為h,那麼有根節點交換次數最多為h次,第二層交換次數最多為(h-1)次,總的交換次數為:
h+(h-1)*2+(h-2)*2^2^+....+2^h^(h-h)=2^h+1^-h-2,可以使用歸納法證明,證明如下:

所以整體的堆排序需要最多2N+2NlgN次比較,2N是構造二叉堆時的比較次數,2NlgN由前文二叉堆構建可知一次下沉操作最多需要2lgN次比較,N個下沉則需要2NlgN次比較。以及一半的交換。

優勢

能夠同時最優的利用好時間和空間,最差情況下也能保證~2NlgN

動態示意圖