1. 程式人生 > >演算法 —— 排序 —— 優先佇列

演算法 —— 排序 —— 優先佇列

優先佇列

【Priority Queue】

首先宣告一下,優先佇列是基於堆的完全二叉樹,它和佇列的概念無關。(它並不是佇列,而是樹

並且,優先佇列最重要的操作就是: 刪除最大元素和插入元素,所以我們把精力集中在這兩點上。(本文以最大堆為主講述)

二叉堆

定義 : 當一棵二叉樹的每個結點都大於等於它的兩個子結點時,它被稱為堆有序。

也可以說根結點(最頂部)是堆有序的二叉樹中的最大結點。(最大堆)

這裡寫圖片描述

如上圖所示,一棵堆有序的完全二叉堆。

我們可以先定下根結點,然後一層一層向下,在每個結點的下方連線兩個更小的結點。完全二叉樹只用陣列就可以表示。

根結點的索引(index)為 1 ,它的子結點在位置 2 和 3 ,並以此類推。

二叉堆是一組能夠用堆有序的完全二叉堆排序的元素,並在陣列中按照層級儲存。(不使用陣列的第一個位置)。

這裡寫圖片描述

在一個堆中,位置 k 的結點的父結點的位置為[ k / 2 ] ,而它的兩個子結點的位置則分別是 2k 或者 2k+1 。

我們用長度為 N+1 的私有陣列 pq[ ] 來表示一個大小為 N 的堆,我們不會使用pq[ 0 ],堆元素放在pq[ 1 ] 至 pq[ N ]中。

插入元素

  • 由下至上的堆有序化(上浮)

如果堆的有序狀態因為某個結點變得比它的父結點更大而被打破,那麼我們需要通過交換它和它的父結點來修復堆。

也就是說通常我們把元素插入在堆(陣列)的末尾,然後不斷的比較或交換它與它的父結點,直至根結點。

這裡寫圖片描述
這裡寫圖片描述
這裡寫圖片描述

    //上浮,插入元素使用。k表示元素索引
    private void swim(int[] a, int k) {
        while (k > 1 && a[k / 2] < a[k]) {
            exch(a, k, k / 2);
            k = k / 2;
        }
    }

如果我們把堆想象成為一個嚴密的黑社會組織,每個每個子結點都表示一個下屬,父結點表示為它的直接上級。
swim() 表示一個很有能力的新人加入了組織並且逐級提升,(將能力不足的上級踩在腳下)直至它遇到了一個更強的領導。

刪除最大元素

  • 由上至下的堆有序化(下沉)

與插入元素相同道理,如果新來的父結點(放置根結點)比子結點小,那麼就需要不斷的交換它和子結點的位置來修復堆。

所以我們刪除最大元素的操作是 :

  • 從陣列頂端刪除最大的元素
  • 將陣列的最後一個元素放到頂端,並讓這個元素下沉( sink )到合適位置。

這裡寫圖片描述
這裡寫圖片描述
這裡寫圖片描述

    /**
     * @param k 元素索引,1開始
     * @param LEN 需排序的元素大小
     */
    private static void sink(int[] a, int k, int LEN) {
        while (2 * k <= LEN) {
            int j = 2 * k;  //j , j + 1  為子結點索引(2個子元素)
            if (j < LEN && a[j] < a[j + 1])
                j++;

            if (a[k] > a[j]) {  //如果新加入的結點大於子結點,則終止
                break;
            }
            exch(a, k, j);  //否則交換父結點與子結點位置
            k = j;  //繼續比較子結點的子結點
        }
    }
    private static int[] delMax(int[] a) {
        exch(a, 1, a.length - 1);// 最大元素和最後一個結點交換
//      a[LEN] = null;// 防止物件遊離,即回收物件
        a = Arrays.copyOf(a, a.length - 1);
        sink(a, 1, a.length - 1);
        return a;
    }

優先佇列由一個基於堆的完全二叉樹表示,儲存於陣列pq[ 1..N ]中,pq [0 ]沒有使用。

  • 插入元素:我們將新元素新增在陣列最後,用swim() (index = LEN-1)恢復堆的秩序。
  • 刪除最大元素 : 我們獲得最大元素(索引為1),並將最大元素和陣列的最後一個元素交換位置,用sink()(index = 1) 恢復堆的秩序。

堆排序

堆排序可以分為兩個階段: 堆構造 和 堆排序

首先我們要將原始陣列重新組織安排進入堆中,使陣列成為堆有序陣列。即堆有序的完全二叉堆。

然後我們再將該陣列進行排序,使陣列成為有序陣列。

1.堆的構造

由N個給定的元素構造一個堆,我們可以很容易的從左至右遍歷陣列,使用swim()將元素一個個插入到堆中。

但是一個更高效的辦法是從右到左,使用sink()函式構造堆,並且我們只需從 LEN / 2 的地方開始往左遍歷即可。

        //堆構造
        for (int k = LEN / 2; k >= 1; k--) {
            sink(a, k, LEN);
        }

這裡寫圖片描述
這裡寫圖片描述
這裡寫圖片描述
這裡寫圖片描述
這裡寫圖片描述
這裡寫圖片描述

2.堆的排序

我們可以再溫習一下刪除最大元素的思想:

  • 我們將堆有序的 第一個元素 pq [ 1 ] 刪除
  • 並且將陣列的最後一放置頂端,使用sink()修復堆。

也就是說我們每次刪除的元素都是最大的,那麼我們只要將依次刪除的這些最大元素收集起來,那麼也就是陣列有序了。

這裡寫圖片描述
這裡寫圖片描述
這裡寫圖片描述
這裡寫圖片描述
這裡寫圖片描述



堆排序的主要工作都是在第二階段(第一階段是堆構造)完成的。

我們將堆的最大元素刪除,然後放入堆縮小後的陣列中空出位置。(這個過程和選擇排序有些類似,但所需的比較要少的多)

public class S_優先佇列 {

    public static void main(String[] args) {
        int[] a = { 0, 9, 6, 8, 10, 2, 11, 1, 5, 7, 4, 2 };// 11個元素,a[0] 不用

        int LEN = a.length - 1;
        for (int k = LEN / 2; k >= 1; k--) {
            sink(a, k, LEN);
        }

        System.out.println("--構造堆--" + Arrays.toString(a));

        while (LEN > 1) {
            exch(a, 1, LEN--);
            sink(a, 1, LEN);
        }

        System.out.println("--堆排序--" + Arrays.toString(a));

//      a = delMax(a);
//      System.out.println("--刪除最大元素--" + Arrays.toString(a));
    }

    /**
     * @param k 元素(非陣列)下標,1開始
     * @param LEN 需排序的元素大小
     */
    private static void sink(int[] a, int k, int LEN) {
        while (2 * k <= LEN) {
            int j = 2 * k;
            if (j < LEN && a[j] < a[j + 1])
                j++;

            if (a[k] > a[j]) {
                break;
            }
            exch(a, k, j);
            k = j;
        }
    }

    /**
     * 上浮,插入元素使用
     */
    private void swim(int[] a, int k) {
        while (k > 1 && a[k / 2] < a[k]) {
            exch(a, k, k / 2);
            k = k / 2;
        }
    }

    private static int[] delMax(int[] a) {
        exch(a, 1, a.length - 1);// 最大元素和最後一個結點交換
//      a[LEN] = null;// 防止物件遊離,即回收物件
        a = Arrays.copyOf(a, a.length - 1);
        sink(a, 1, a.length - 1);
        return a;
    }

    public static void exch(int[] arr, int a, int b) {
        int temp = arr[a];
        arr[a] = arr[b];
        arr[b] = temp;
    }
}

總結

堆排序在排序複雜度的研究中有著重要的地位。因為它在最壞的情況下也能保證使用 ~2NlgN 次比較和恆定的額外空間。

並且它能在 插入操作刪除最大元素操作混合動態場景中保證對數級別的執行時間。

延伸 —– 最小堆

另外本文所述的為最大堆(大頂堆),即根結點最大。如果根結點為最小,它的子結點都比父結點大,那麼稱為最小堆(小頂堆)

從 n 個無序數中選出 m 個最大數

  • 最小堆 : 頭 m 個數建立 size = m 的最小堆,(根結點最小),後續數小於根結點則拋棄;大於根結點則插入並替換。

  • 快排 : 當大於分界的個數大於 m ,則拋棄小於分界的部分。當大於分界的個數小於 m ,則全部保留,並且在小於的部分找出剩餘的 n - m 個最大數。