排序算法終極彙總

本文對9種排序方法進行彙總。
分別是: 插入排序 選擇排序 歸併排序 冒泡排序 堆排序 快排序 計數排序 基數排序 桶排序。
參照《算法》第四版這本書,把排序須要的公共的方法抽象出來,作一個抽象類,討論到的各個排序類對抽象類進行繼承,只需關注與排序自己的業務邏輯便可。
https://visualgo.net/sortinghtml

抽象出來的父類爲:git

abstract Sort{
    abstract void sort(array);     // 須要被實現
    void exchange(array, i, j);    // 交換數組中的i 和j位置的元素
    boolean less(a, b);            // a是否小於b
    boolean isSorted(array);       // 數組是否已排好序
    void test(arr);                // 對傳入的數組進行測試
}

對應的Java實現web

/**
 1. 排序的抽象類
 2.         能夠接受任意類型,能夠自定義比較器
 3. @param <T>
 */
public abstract class Sort<T> {
    /** 測試數組,這裏爲了方便使用整型數組*/
    protected static Integer[] testArray = { 3, 2, 5, 1, 4, 7 ,10};
    /** 繼承該類須要實現排序方法*/
    public abstract void sort(Comparable<T>[] array);
    /** 交換數組元素的業務方法*/
    protected void exchange(Comparable<T>[] array, int i, int j){
        Comparable<T> temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }
    /** 比較兩個元素的方法*/
    protected boolean less(Comparable<T> a, Comparable<T> b){
        return a.compareTo((T) b) < 0;
    }
    /** 判斷數組是否已排序的方法*/
    protected boolean isSorted(Comparable<T>[] array){
        for(int i = 1; i<array.length; i++)
            if(less(array[i],array[i-1]))    return false;
        return true;
    }
    /** 測試方法,爲了方便把測試方法也寫進了父類,子類實現完畢後能夠直接調用看結果*/
    protected void test(Comparable<T>[] arr){
        //輸出排序前的數組
        System.out.println(Arrays.toString(arr));
        //排序
        sort(arr);
        //輸出排序後的結果
        System.out.println(Arrays.toString(arr));
        //輸出是否已經排序
        System.out.println("是否已經排序:" + isSorted(arr));
    }
}

1.插入排序

  1. 時間O(n^2);空間O(1);算法

  2. 排序時間與輸入有關:輸入的元素個數,輸入元素已排序程度;api

  3. 最好狀況:輸入數組已是排序的,時間變爲n的線性函數;數組

  4. 最壞狀況:輸入數組是逆序,時間是n的二次函數緩存

/**
 * 插入排序
 */
public class InsertSort<T> extends Sort<T> {
    @Override
    public void sort(Comparable<T>[] array) {
        int len = array.length;
        // 把a[i] 插入到a[i-1], a[i-2], a[i-3]...中
        for (int i = 1; i < len; i++) {
            // j從i開始,若是j>0而且j處元素比前面的元素小,則進行交換,j--,繼續向前比較
            for (int j = i; j > 0 && less(array[j], array[j-1]); j--)
                exchange(array, j, j-1);
        }
    }

    public static void main(String[] args) {
        new InsertSort().test(testArray);
    }
}

結果:數據結構

[3, 2, 5, 1, 4, 7, 10]
[1, 2, 3, 4, 5, 7, 10]
是否已經排序:true

2.選擇排序

  • 時間O(n^2),空間O(1)less

  • 排序時間和輸入無關dom

  • 最好和最壞都是同樣的

  • 不穩定,例如{6, 6, 1}.找到最小的是1,和第一個6交換之後,第一個6跑到了後面.

/**
 * 選擇排序
 */
public class SelectionSort<T> extends Sort<T>{
    @Override
    public void sort(Comparable<T>[] array) {
        int len = array.length;
        for(int i = 0; i<len; i++){
            int min = i;
            //左邊已經排好序,每次從i+1開始找到最小值,並記錄位置
            for(int j=i+1; j<len; j++){
                if(less(array[j], array[min]))
                    min = j;    // 記錄最小值的位置
            }
            exchange(array, min, i);//內循環結束後最小值和i進行交換,確保左邊依舊是排好序的狀態
        }
    }
    public static void main(String[] args) {
        new SelectionSort().test(testArray);
    }
}

3.歸併排序

  • 歸併排序的全部算法都基於歸併這個簡單的操做,即將兩個有序的數組歸併稱爲一個更大的有序數組。

  • 發現這個算法的由來:要將一個數組排序,能夠先遞歸地將它分紅兩半分別排序,而後將結果歸併起來。

  • 性質:可以保證將任意長度爲N的數組排序,所需時間和NlogN成正比;

  • 缺點:所需額外空間和N成正比。

  • 排序時間和輸入無關,最佳狀況最壞狀況都是如此,穩定。
    圖片描述

3.1自頂向下的歸併排序算法

/**
* 歸併排序:自頂向下
*         分治思想的最經典的一個例子。
*         這段遞歸代碼是概括證實算法可以正確地將數組排序的基礎:
*             若是它能將兩個子數組排序,它就能經過歸併兩個子數組來說整個數組排序
*/
public class MergeSort<T> extends Sort<T>{
    private static Comparable[] auxiliary;
    @Override
    public void sort(Comparable[] array) {
        auxiliary = new Comparable[array.length];
        sort(array, 0, array.length-1);
    }
    
    private void sort(Comparable[] array, int low, int high) {
        if(high <= low)        return;
        int mid = low + (high - low) / 2;
        sort(array, low, mid);        //將左半邊排序
        sort(array, mid + 1, high);    //將右半邊排序
        merge(array, low, mid, high);//歸併結果
    }

    private void merge(Comparable[] a, int low, int mid, int high){
        // 將a[low...mid]和a[mid+1...high]歸併
        int i = low, j = mid + 1;
        // 先將全部元素複製到aux中,而後再歸併會a中。
        for(int k = low; k <= high; k++)
            auxiliary[k] = a[k];
        for(int k = low; k <= high; k++)//歸併回到a[low...high]
            if(i > mid)    
                a[k] = auxiliary[j++];    // 左半邊用盡,取右半邊的元素
            else if    (j > high)
                a[k] = auxiliary[i++];    // 右半邊用盡,取左半邊的元素
            else if    (less(auxiliary[j], auxiliary[i]))
                a[k] = auxiliary[j++];    // 右半邊當前元素小於左半邊當前元素,取右半邊的元素
            else    
                a[k] = auxiliary[i++];    // 左半邊當前元素小於又半邊當前元素,取左半邊的元素
    }
    public static void main(String[] args) {
        new MergeSort().test(testArray);
    }
}

對於16個元素的數組,其遞歸過程以下:
圖片描述

這個NLogN的時間複雜度和插入排序和選擇排序不可同日而語,它代表只需比遍歷整個數組多個對數因子的時間就能將一個龐大的數組排序。能夠用歸併排序處理百萬甚至更大規模的數組,這是插入和選擇排序所作不到的。
其缺點是輔助數組所使用的額外空間和N的大小成正比。
另外經過一些細緻的思考,還能夠大幅度縮短歸併排序的運行時間。

  • 考慮1:對小規模子數組使用插入排序。使用插入排序處理小規模的子數組(好比長度小於15)通常能夠將歸併排序運行時間縮短10%-15%。

  • 考慮2:測試數組是否已經有序。能夠添加一個判斷條件,若是array[ mid ] <= array[ mid + 1 ]就認爲數組已是有序的,並跳過merge方法,這個改動不影響排序的遞歸調用,可是任意有序的子數組算法運行的時間就變成線性了。

  • 考慮3:不將元素複製到輔助數組。能夠節省將元素複製到用於歸併的輔助數組所用的時間(但空間不行)。要作到這一點須要調用兩種排序方法,一種將數據從輸入屬豬排序到輔助數組,一種將數據從輔助數組排序到輸入數組。


3.2 自底向上的歸併排序
先歸併那些微型數組,而後再成對歸併獲得的子數組,如此這般,直到將整個數組歸併在一塊兒。
該實現比標準遞歸方法代碼量少。
首先進行兩兩歸併,而後四四歸併,八八歸併,一直下去。在每一輪歸併中,最後一次歸併的第二個子數組可能比第一個要小,可是對merge方法不是問題,若是不是的話全部的歸併中兩個數組的大小都應該同樣,而在下一輪中子數組的大小翻倍。如圖:
圖片描述

/**
 * 自底向上的歸併排序
 *         會屢次遍歷整個數組,根據子數組大小進行兩兩歸併。
 *         子數組的大小size初始值爲1,每次加倍。
 *         最後一個子數組的大小隻有在數組大小是size的偶數被時纔會等於size,不然會比size小。
 * @param <T>
 */
public class MergeSortBU<T> extends Sort<T>{
    private static Comparable[] aux;
    @Override
    public void sort(Comparable<T>[] a) {
        int n = a.length;
        aux = new Comparable[n];
        //進行lgN次兩兩歸併
        for(int size = 1; size < n; size = size + size)
            for(int low = 0; low < n - size; low += size+size)
                merge(a, low, low+size-1, Math.min(low+size + size-1, n-1));
    }
    @SuppressWarnings("unchecked")
    private void merge(Comparable<T>[] a, int low, int mid, int high){
        int i = low, j = mid + 1;
        for(int k = low; k <= high; k++)
            aux[k] = a[k];
        for(int k = low; k <= high; k++){
            if(i > mid)
                a[k] = aux[j++];
            else if(j > high)
                a[k] = aux[i++];
            else if(less(a[j], a[i]))
                a[k] = aux[j++];
            else
                a[k] = aux[i++];
        }
    }
    public static void main(String[] args) {
        new MergeSortBU<Integer>().test(testArray);
    }
}

若是是排序16個元素的數組,過程以下圖
圖片描述

4.冒泡排序

比較簡單

/**
 * 冒泡排序
 * 時間:O(n^2);空間O(1)
 * 穩定,由於存在兩兩比較,不存在跳躍
 * 排序時間與輸入無關
 */
public class BubbleSort<T> extends Sort<T> {
    @Override
    public void sort(Comparable[] array) {
        int len = array.length;
        for(int i = 0; i<len-1; i++){
            for(int j = len-1; j>i; j--){
                if(less(array[j], array[j-1]))
                    exchange(array, j, j-1);
            }
        }
    }
    public static void main(String[] args) {
        new BubbleSort<Integer>().test(testArray);
    }
}

缺陷:

  1. 排序過程當中,執行完第i趟排序後,可能數據已所有排序完畢,可是程序沒法判斷是否完成排序,會繼續執行剩下的(n-1-i)趟排序。解決方法:設置一個flag位,若是一趟無元素交換,則flag=0;之後不再進入第二層循環。

  2. 當排序的數據比較多時,排序的時間會明顯延長,由於會比較n*(n-1)/2次。


5. 快排序

  1. 快排序
    實現簡單,適用於各類不一樣輸入,通常應用中比其餘算法快不少。

特色:原地排序(只需很小的一個輔助棧);時間和NlgN成正比。同時具有這兩個優勢。

另外,快排序的內循環比大多數排序算法都短。

5.1 基本算法
快排序是一種分治的算法,將一個數組分紅兩個子數組,將兩部分獨立排序。
快排序和歸併排序互補:歸併排序將數組分紅兩個子數組分別排序,並將有序的子數組歸併以將整個數組排序;
而快排序將數組排序的方式是當兩個子數組都有序的時候,整個數組天然也就有序了。
第一種狀況,遞歸調用發生在處理整個數組以前;第二種狀況,遞歸發生在處理整個數組以後。
歸併排序中,一個數組被等分爲兩半;快排序中,切分的位置取決於數組的內容。
圖片描述

/**
 * 快排序
 */
public class QuickSort<T> extends Sort<T> {
    @Override
    public void sort(Comparable<T>[] array) {
        shuffle(array);
        System.out.println("打亂後:"+Arrays.toString(array));
        sort(array, 0, array.length - 1);
    }
    private void sort(Comparable<T>[] array, int low, int high) {
        if(high <= low)    return;
        int j = partition(array, low, high);    // 切分
        sort(array, low, j-1);            // 將左半部分array[low, ... , j-1]進行排序
        sort(array, j+1, high);            // 將右半部分array[j+1, ... , high]進行排序
    }
    private int partition(Comparable<T>[] array, int low, int high) {
        // 將數組切分爲array[low, ... , i-1], array[i], array[i+1, ... , high]
        int i = low, j = high+1;        //左右掃描指針
        Comparable v = array[low];    
        while(true){
            //掃描左右,檢查掃描是否結束並交換元素
            while(less(array[++i], v))    if(i == high)    break;//左指針向右找到一個大於v的位置
            while(less(v, array[--j]))    if(j == low)    break;//右指針向左找到一個小於v的位置
            if(i >= j)    break;    // 若是左指針重疊或者超過右指針,跳出
            exchange(array, i, j);  // 交換左右指針位置的元素
        }
        exchange(array, low, j);
        return j;
    }
    private void shuffle(Comparable<T>[] a){
        Random random = new Random();
        for(int i = 0; i<a.length;i++){
            int r = i + random.nextInt(a.length - i);
            exchange(a, i, r);
        }
    }
    public static void main(String[] args) {
        new QuickSort<Integer>().test(testArray);
    }
}

這段代碼按照array[low] 的值v進行切分。當指針i和j相遇時主循環退出。在循環中,array[i]小於v時,增大i,a[j]大於v時,減少j,而後交換array[i]和array[j]來保證i左側的元素都不大於v,j右側的元素都不小於v。當指針相遇時交換array[low]和array[j],切分結束,這樣切分值就留在array[j]中了。
圖片描述


5.2快排序算法的改進:
若是排序代碼會被執行不少次,或者會被用在大型數組上(特別是若是被髮布成一個庫函數,排序的對象數組的特性是未知的),那麼須要提高性能。
如下改進會將性能提高20%~30%。

  1. 切換到插入排序

  • 對於小數組,快排序比插入排序慢

  • 由於遞歸,快排序的Sort方法在小數組中也會調用本身
    基於這兩點能夠改進快排序。改動以上算法,將sort()方法中的語句

if(high <= low) return ;
替換爲:
if(high <= low + M) { Insersion.sort(array, low, high); return; }
轉換參數M的最佳值和系統相關,5~15之間的任意值在大多狀況下都使人知足。

  1. 三取樣切分
    使用子數組的一小部分元素的中位數來切分數組。這樣作獲得的切分更好,可是代價是須要計算中位數。

發現將取樣大小設置爲3並用大小居中的元素切分的效果最好。
還能夠將取樣元素放到數組末尾做爲哨兵來去掉partition()中的數組邊界測試。

  1. 熵最優的排序
    在有大量重複元素的狀況下,快速排序的遞歸性會使元素所有重複的子數組常常出現,這樣就有很大的改進潛力,能提升到線性級別。

簡單的想法:將數組且分爲3部分,分別對應小於,等於和大於切分元素的數組袁術。這種切分實現起來比目前的二分法更復雜。

/**
 * 快排序:三項切分的快速排序
 */
public class Quick3WaySort<T> extends Sort<T> {
    @Override
    public void sort(Comparable<T>[] array) {
        shuffle(array);
        System.out.println("打亂後:"+Arrays.toString(array));
        sort(array, 0, array.length - 1);
    }
    private void sort(Comparable[] array, int low, int high) {
        if(high <= low)    return;
        int lt = low, i = low + 1, gt = high;
        Comparable<T> v = array[low];
        while(i <= gt){
            int cmp = array[i].compareTo(v);
            if(cmp < 0)        exchange(array, lt++, i++);
            else if(cmp > 0)    exchange(array, i, gt--);
            else            i++;
        } // 如今array[low ... lt-1] < v = a[lt ... gt] < array[gt+1 .. high]成立
        sort(array, low, lt-1);
        sort(array, gt+1, high);
    }
    private void shuffle(Comparable<T>[] a){
        Random random = new Random();
        for(int i = 0; i<a.length;i++){
            int r = i + random.nextInt(a.length - i);
            exchange(a, i, r);
        }
    }
    public static void main(String[] args) {
        Integer[] chars = {18,2,23,23,18,23,2,18,18,23,2,18}; 
        new Quick3WaySort<Integer>().test(chars);
    }
}

6.堆排序

時間複雜度O(nlogn), 空間複雜度O(1), 是一種原地排序。
排序時間和輸入無關,不穩定。
對於大數據處理:若是對於100億條數據選擇 top K 的數據,只能用堆排序。堆排序只須要維護一個k大小的空間,即在內存開闢k大小的空間。
而不能選擇快速排序,由於快排序要開闢1000億條數據的空間,這個是不可能的。

這裏先來看算法第四版這本書中的2.4節:優先級隊列
應用舉例:絕大多數手機分配給來電的優先級都會比其餘應用高。
數據結構:優先級隊列,需支持兩種操做 刪除最大元素和插入元素。
本節中簡單討論優先級隊列的基本表現形式,其一或者兩種操做都能在線性時間內完成。以後學習基於二叉堆結構的一中優先級隊列的經典實現方法,
用數組保存元素並按照必定條件排序,以實現高效刪除最大元素和插入元素的操做(對數級別)。
堆排序算法也來自於基於堆的優先級隊列的實現。

  • 稍後學習用優先級隊列構造其餘算法。

  • 也能恰到好處的抽象若干重要的圖搜索算法(算法第四版第四章)。

  • 也能夠開發出一種數據壓縮算法(算法第四版第五章)。

6.1API的設計
圖片描述

三個構造函數使得用例能夠構造制定大小的優先級隊列,還能夠用給定的一個數組將其初始化。
會在適當的地方使用另外一個類MinPQ, 和MaxPQ相似,只是含有一個delMin()方法來刪除並返回最小元素。
MaxPQ的任意實現都能很容易轉化爲MinPQ的實現,反之亦然,只須要改變一下less()比較的方向便可。

優先級隊列的調用示例
爲了展現優先級隊列的價值,考慮問題:輸入N個字符串,每一個字符串都對應一個整數,找出最大的或最小的M個整數(及其關聯的字符串)。
例如:輸入金融事務,找出最大的那些;農產品中殺蟲劑含量,找出最小的那些。。。
某些場景中,輸入量多是巨大的,甚至能夠認爲輸入是無限的。
解決這個問題,

  • 一種方法是將輸入排序,而後從中找出M個最大元素。

  • 另外一種方法,將每一個新的輸入和已知的M個最大元素比較,但除非M較小,不然這種比較代價高昂。

  • 使用優先級隊列,這種纔是正解,只要高效的實現insert和delMin方法便可。
    三種方法的成本:

圖片描述

看一個優先級隊列的用例
圖片描述

命令行輸入一個整數M以及一系列字符串,每一行表示一個事務,代碼調用MinPQ並打印數字最大的M行。

初級實現:可使用有序數組,無序數組,鏈表。
圖片描述

堆的定義:二叉堆可以很好的實現優先級隊列的基本操做。

  • 當一顆二叉樹的每一個結點都大於等於它的兩個子結點時,被稱爲堆有序。

  • 根節點是堆有序的二叉樹中的最大節點。
    二叉堆:一組可以用堆有序的徹底二叉樹排序的元素,並在數組中按層級存儲(不使用數組的第0個位置)

在一個堆中,位置K的節點的父節點位置爲 K/2 向下取整,兩個子節點的位置分別是2K和2K+1。這樣能夠在不使用指針的狀況下經過計算數組的索引在樹中上下移動:從a[k]向上一層,就令k = k/2,向下一層就令k = 2k 或者2k+1。
圖片描述

用數組實現的徹底二叉樹結構嚴格,但其靈活性足以讓咱們高效的實現優先級隊列。
可以實現對數級別的插入元素和刪除最大元素的操做。利用數組無需指針便可沿着樹上下移動的遍歷和如下性質,保證了對數複雜度的性能。
命題:一顆大小爲N的徹底二叉樹的高度爲lgN向下取整。

堆的算法:

  • 用長度爲N+1的私有數組pq[]來表示一個大小爲N的堆,不使用pq[0],對元素放在pq[1]—pq[n]中。

  • 在以前的排序中,經過輔助函數less和exchange函數來訪問元素,但由於全部的元素都在數組pq中,該實現爲了更加緊湊,再也不將數組做爲參數傳遞。

  • 堆的操做首先進行一些簡單的改動,打破堆的狀態,而後再遍歷堆並按照要求將堆的狀態恢復。這個過程叫作堆的有序化(reheapifying)

比較和交換方法:
圖片描述

可能遇到的兩種狀況:

由下至上的堆有序化(上浮)
若是堆的有序狀態由於某個節點變得比它的父節點更大而被打破,那麼須要經過交換它和父節點位置來修復堆。交換後,這個節點比它的兩個子節點都大,可是仍然可能比它如今的父節點大,能夠一遍遍的用一樣的方法恢復秩序,這個節點不斷上移知道遇到一個更大的父節點。只要記住位置K的節點的父節點的位置是K/2,該過程實現簡單。
圖片描述

由上至下的堆有序化(下沉)
若是有序狀態由於某個節點變得比兩個子節點或是其中之一更小而被打破,那麼能夠經過將它和兩個子節點中的較大者交換來恢復有序狀態。交換可能會在子節點出繼續打破有序狀態,所以須要不斷用相同方法來修復,將節點向下移動知道它的子節點都比它更小或者到達了對的地步。由位置K的節點的子節點位於2K和2K+1處,能夠實現代碼。

例子:能夠想象堆是一個嚴密的黑社會組織,每一個子節點都表示一個下屬,父節點表示它的直接上級。swim表示一個頗有能力的新人加入組織並被逐級提高(將能力不夠的上級踩在腳下),直到遇到一個更強的領導。sink則相似於整個社團的領導退休並被外來者取代後,若是他的下屬比他更厲害,他們的角色就會交換,這種交換會持續下去直到他的能力比其餘下屬都強爲止。

sink和swim方法是高效實現優先級隊列API的基礎。
圖片描述

插入元素:新元素加到數組末尾;增長堆的大小;新元素上浮到合適的位置。
刪除最大元素:從數組頂端刪去最大的元素;並將數組的最後一個元素放到頂端;減少堆的大小;並讓該元素下沉到合適的位置。

該算法對API的實現可以保證插入元素和刪除最大元素這兩個操做的用時和隊列大小僅呈對數關係。

圖片描述

命題:對於一個含有N個元素的基於堆的優先級隊列,插入元素操做只須要不超過lgN+1次比較,刪除最大元素的操做須要不超過2lgN次比較。兩種操做都須要在根節點和堆底之間移動元素,而路徑的長度不超過lgN。對於路徑上的每一個節點,刪除最大元素須要比較兩次(除了堆底元素),一次用來找出較大的子節點,一次用來肯定該子節點是否須要上浮。

多叉堆

  • 構建徹底三叉樹結構
    調整數組大小

  • 添加無參構造函數,在insert中添加將數組加倍的代碼,在delMax中添加將數組長度減半的代碼。
    元素的不可變性

  • 優先級隊列存儲了用例建立的對象,但同時假設用例代碼不會改變它們。可將這個假設轉化爲強制條件,但增長代碼的複雜性會下降性能。
    索引優先級隊列

不少應用中,容許用例引用已進入優先級隊列中的元素頗有必要。

  • 作到這一點的一種簡單方法是給每一個元素一個索引。

  • 另外,一種常見的狀況是用例已經有了總量爲N的多個元素,並且可能還同時使用了多個平行數組來存儲這些元素的信息。此時其餘無關的用例代碼可能已經在使用一個整數索引來引用這些元素了。
    這些考慮引導咱們設計了下列API。

圖片描述

將它當作一個可以快速訪問其中最小元素的數組。
事實上還更好:可以快速訪問數組的一個特定子集中的最小元素(指全部被插入的元素)。
換句話說:

  • 可將名爲pq的IndexMinPQ類優先級隊列看作數組pq[0...n-1]中的一部分元素的表明。

  • 將pq.insert(k,item)看作將k加入這個子集並使得pq[k]=item,

  • pq.change(k, item)則表明令pq[k]=item。

  • 這兩種操做沒有改變其餘操做所依賴的數據結構,其中最重要的就是delMin()(刪除最小元素並返回它的索引)和change()(改變數據結構中的某個元素的索引—即pq[i]=item)。這些操做在許多應用中都很重要而且依賴於對元素的引用(索引)
    命題:在一個大小爲N的索引優先級隊列中,插入元素insert、改變優先級change、刪除delete和刪除最小元素remove the minimum 這些操做所需的比較次數和lgN成正比。

圖片描述

此處留坑,之後再看,這是庫中的源碼

/**
 * 索引優先級隊列IndexMinPQ
 */
public class IndexMinPQ<Key extends Comparable<Key>> implements Iterable<Integer> {
    private int maxN;        // maximum number of elements on PQ
    private int n;           // number of elements on PQ
    private int[] pq;        // binary heap using 1-based indexing
    private int[] qp;        // inverse of pq - qp[pq[i]] = pq[qp[i]] = i
    private Key[] keys;      // keys[i] = priority of i
    public IndexMinPQ(int maxN) {
        this.maxN = maxN;
        n = 0;
        keys = (Key[]) new Comparable[maxN + 1];    // make this of length maxN??
        pq  = new int[maxN + 1];
        qp  = new int[maxN + 1];                   // make this of length maxN??
        for (int i = 0; i <= maxN; i++)
            qp[i] = -1;
    }
    public boolean isEmpty() {return n == 0;}

    public boolean contains(int i) {return qp[i] != -1;}

    public int size() { return n;}

    public void insert(int i, Key key) {
        if (i < 0 || i >= maxN) throw new IndexOutOfBoundsException();
        if (contains(i)) throw new IllegalArgumentException("index is already in the priority queue");
        n++;
        qp[i] = n;
        pq[n] = i;
        keys[i] = key;
        swim(n);
    }
    public int minIndex() {
        if (n == 0) throw new NoSuchElementException("Priority queue underflow");
        return pq[1];
    }

    public Key minKey() {
        if (n == 0) throw new NoSuchElementException("Priority queue underflow");
        return keys[pq[1]];
    }

    public int delMin() {
        if (n == 0) throw new NoSuchElementException("Priority queue underflow");
        int min = pq[1];
        exch(1, n--);
        sink(1);
        assert min == pq[n+1];
        qp[min] = -1;        // delete
        keys[min] = null;    // to help with garbage collection
        pq[n+1] = -1;        // not needed
        return min;
    }

    public Key keyOf(int i) {
        if (i < 0 || i >= maxN) throw new IndexOutOfBoundsException();
        if (!contains(i)) throw new NoSuchElementException("index is not in the priority queue");
        else return keys[i];
    }

    public void changeKey(int i, Key key) {
        if (i < 0 || i >= maxN) throw new IndexOutOfBoundsException();
        if (!contains(i)) throw new NoSuchElementException("index is not in the priority queue");
        keys[i] = key;
        swim(qp[i]);
        sink(qp[i]);
    }

    public void decreaseKey(int i, Key key) {
        if (i < 0 || i >= maxN) throw new IndexOutOfBoundsException();
        if (!contains(i)) throw new NoSuchElementException("index is not in the priority queue");
        if (keys[i].compareTo(key) <= 0)
            throw new IllegalArgumentException("Calling decreaseKey() with given argument would not strictly decrease the key");
        keys[i] = key;
        swim(qp[i]);
    }

    public void increaseKey(int i, Key key) {
        if (i < 0 || i >= maxN) throw new IndexOutOfBoundsException();
        if (!contains(i)) throw new NoSuchElementException("index is not in the priority queue");
        if (keys[i].compareTo(key) >= 0)
            throw new IllegalArgumentException("Calling increaseKey() with given argument would not strictly increase the key");
        keys[i] = key;
        sink(qp[i]);
    }

    public void delete(int i) {
        if (i < 0 || i >= maxN) throw new IndexOutOfBoundsException();
        if (!contains(i)) throw new NoSuchElementException("index is not in the priority queue");
        int index = qp[i];
        exch(index, n--);
        swim(index);
        sink(index);
        keys[i] = null;
        qp[i] = -1;
    }


    private boolean greater(int i, int j) {return keys[pq[i]].compareTo(keys[pq[j]]) > 0;}

    private void exch(int i, int j) {
        int swap = pq[i];
        pq[i] = pq[j];
        pq[j] = swap;
        qp[pq[i]] = i;
        qp[pq[j]] = j;
    }
    private void swim(int k) {
        while (k > 1 && greater(k/2, k)) {
            exch(k, k/2);
            k = k/2;
        }
    }

    private void sink(int k) {
        while (2*k <= n) {
            int j = 2*k;
            if (j < n && greater(j, j+1)) j++;
            if (!greater(k, j)) break;
            exch(k, j);
            k = j;
        }
    }
    public Iterator<Integer> iterator() { return new HeapIterator(); }

    private class HeapIterator implements Iterator<Integer> {
        // create a new pq
        private IndexMinPQ<Key> copy;
        // add all elements to copy of heap
        // takes linear time since already in heap order so no keys move
        public HeapIterator() {
            copy = new IndexMinPQ<Key>(pq.length - 1);
            for (int i = 1; i <= n; i++)
                copy.insert(pq[i], keys[pq[i]]);
        }

        public boolean hasNext()  { return !copy.isEmpty();                     }
        public void remove()      { throw new UnsupportedOperationException();  }

        public Integer next() {
            if (!hasNext()) throw new NoSuchElementException();
            return copy.delMin();
        }
    }

    public static void main(String[] args) {
        // insert a bunch of strings
        String[] strings = { "it", "was", "the", "best", "of", "times", "it", "was", "the", "worst" };
        IndexMinPQ<String> pq = new IndexMinPQ<String>(strings.length);
        for (int i = 0; i < strings.length; i++) 
            pq.insert(i, strings[i]);
        // delete and print each key
        while (!pq.isEmpty()) {
            int i = pq.delMin();
            StdOut.println(i + " " + strings[i]);
        }
        StdOut.println();
        // reinsert the same strings
        for (int i = 0; i < strings.length; i++) 
            pq.insert(i, strings[i]);
        // print each key using the iterator
        for (int i : pq) 
            StdOut.println(i + " " + strings[i]);
    }
}

索引優先級隊列用例:
多向歸併問題:將多個有序的輸入流歸併成一個有序的輸入流。

  • 輸入流可能來自多種一塊兒的輸出(按時間排序),

  • 或者來自多個音樂或電影網站的信息列表(按名稱或者藝術家名字排序),

  • 或是商業交易(按帳號或時間排序)。

  • 若是有足夠的空間,能夠簡單地讀入一個數組並排序,但用了優先級隊列不管輸入有多長你均可以把它們所有讀入並排序。

/**
 * 使用優先隊列的多項歸併
 */
public class Multiway {
    public static void merge(In[] streams){
        int n = streams.length;
        IndexMinPQ<String> pq = new IndexMinPQ<String>(n);
        for(int i = 0;i<n;i++){
            if(!streams[i].isEmpty()){
                String s = streams[i].readString();
                pq.insert(i, s);
            }
        }
        while(!pq.isEmpty()){
            StdOut.print(pq.minKey()+" ");
            int i = pq.delMin();
            if(!streams[i].isEmpty()){
                String s = streams[i].readString();
                pq.insert(i, s);
            }
        }
    }
    
    public static void main(String[] args) {
        ClassLoader loader = Multiway.class.getClassLoader();
        String dir = Multiway.class.getPackage().getName().replace(".", "/");
        String path0 = loader.getResource(dir+"/m1.txt").getPath();
        String path1 = loader.getResource(dir+"/m2.txt").getPath();
        String path2 = loader.getResource(dir+"/m3.txt").getPath();

        String[] paths = {path0, path1, path2};
        int n = 3;
        In[] streams = new In[n];
        for(int i = 0;i<n;i++){
            streams[i] = new In(new File(paths[i]));
        }
        merge(streams);
    }
}
結果
A A B B B C D E F F G H I I J N P Q Q Z

結果有了上面的擴展知識,下面來看堆排序:
能夠把任意優先級隊列變成一種排序方法。將全部元素插入一個查找最小元素的優先級隊列,而後重複調用刪除最小元素的操做將它們按順序刪除。
堆排序分爲兩個階段。構造階段中,將原始數組從新組織安排進一個堆中;而後在下沉排序階段,從堆中按遞減順序取出全部元素並獲得排序結果。
爲了排序須要,再也不將優先級隊列的具體表示隱藏,將直接使用swim和sink操做。這樣在排序時就能夠將須要排序的數組自己做爲堆,所以無需任何額外空間。

/**
 * 堆排序
 */
public class HeapSort {
    public static void sort(Comparable[] a){
        int n = a.length - 1; // index=0的位置不使用, n是最後一個index
        buildHeap(a, n);
        while(n>1){
            exchange(a,1,n--);
            sink(a,1,n);
        }
    }
    /**
     * 構造堆
     */
    private static void buildHeap(Comparable[] a, int n) {
        for(int k = n/2; k>=1; k--)    
            sink(a, k, n);
    }

    private static void exchange(Comparable[] a, int i, int j) {
        Comparable temp = a[i];
        a[i] = a[j];
        a[j] = 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;
            exchange(a,k,j);
            k = j;
        }
    }
    private static boolean less(Comparable[] a, int i, int j){
        return a[i].compareTo(a[j])<0;
    }
    public static void main(String[] args) {
        // index=0的位置不使用
        String[] strings = { " ", "s","o", "r", "t", "e", "x", "a", "m", "p", "l", "e" };
        sort(strings);
        System.out.println(Arrays.toString(strings));
    }
}

結果:

[ , a, e, e, l, m, o, p, r, s, t, x]
  • 該算法用sink方法將a[1]到a[n]的元素排序(n=len-1),sink接受的參數須要修改。

  • for循環構造堆,while循環將最大元素a[1]和a[n]交換並修復堆,如此重複直到堆變爲空

  • 調用exchange時索引減一便可

圖片描述

下圖是堆的構造和下沉過程:
圖片描述

堆排序的主要工做是在第二階段完成的。

  • 刪除堆中最大元素

  • 放入堆縮小後數組空出的位置。

  • 進行下沉操做。
    命題R:用下沉操做由N個元素構造堆 只須要少於2N次比較以及少於N次交換。

命題S:將N個元素排序,堆排序只須要少於(2NlgN+2N)次比較(以及一半次數的交換)。
第一次for循環構造堆,第二次while循環在下沉排序中銷燬堆。都是基於sink方法。
將該實現和優先級隊列的API獨立開是爲了突出這個排序算法的簡潔性,構造和sink分別只需幾行代碼。

堆排序在排序複雜性的研究中有很重要的地位,是所知的惟一可以同時最優的利用空間和時間的方法。
最壞狀況也能保證2NlgN次比較和恆定的額外空間。空間緊張時很流行。
可是現代系統許多應用不多使用它,由於它沒法利用緩存。數組元素不多和相鄰元素進行比較,所以緩存Miss遠遠高於大多數比較都在相鄰元素之間進行的算法。



上面的幾種排序方法都是基於比較排序的算法。時間複雜度下界是O(nlogn)

下面介紹的三種排序是非基於比較的算法。計數排序,桶排序,基數排序。是能夠突破O(nlogn)的下界的。
可是非基於比較的排序算法使用限制比較多。

  • 計數排序進對較小整數進行排序,且要求排序的數據規模不能過大

  • 基數排序能夠對長整數進行排序,可是不適用於浮點數。

  • 桶排序能夠對浮點數進行排序
    下面一一來學習。

7.計數排序

在排序的時候就知道其位置,那麼就掃描一遍放入正確位置。如此以來,只需知道有多大範圍就能夠了。這就是計數排序的思想。

性能:時間複雜度O(n+k),線性時間,而且穩定!
優勢:不需比較,利用地址偏移,對範圍固定在[0,k]的整數排序的最佳選擇。是排序字符串最快的排序算法
缺點:用來計數的數組的長度取決於帶排序數組中數據的範圍(等於待排序數組的最大值和最小值的差加1),這使得計數排序對於數據範圍很大的數組,須要大量時間和空間。

/**
 * 計數排序
 */
public class CountSort {
    public static int[] sort(int[] array){
        int[] result = new int[array.length];    // 存儲結果
        int max = max(array);                // 找到待排序數組中的最大值max
        int[] temp = new int[max+1];         // 申請一個大小爲max+1的輔助數組
        for(int i = 0; i<array.length;i++)    // 遍歷待排序數組
            temp[array[i]] = temp[array[i]] + 1;    //以當前值做爲索引,把輔助數組索引位置的值自增1
        
        for(int i = 1; i<temp.length;i++)    // 輔助數組從index=1開始遍歷
            temp[i] = temp[i] + temp[i-1];  // 當前值+前一個元素的值,賦值給當前值。以此來幫助計算result放置的位置
        // 逆序輸出確保穩定--保證相同因素的相對順序
        for(int i = array.length - 1; i>=0; i--){
            int v = array[i];            // 當前元素
            result[temp[v] - 1] = v;    // 當前元素做爲索引,獲得輔助數組元素,減一後的結果做爲result中的索引,該處放置當前的遍歷元素
            temp[v] = temp[v] - 1;        // 輔助數組相應位置減小1,以供下個相同元素索引到正確位置
        }
        return result;
    }
    private static int max(int[] array) {
        int max = array[0];
        for(int i = 1; i < array.length; i++)
            if(array[i] > max)    max = array[i];
        return max;
    }
    public static void main(String[] args) {
        int[] arr = {3,4,1,7,2,8,0};
        int[] result = sort(arr);
        System.out.println(Arrays.toString(result));
    }
}

http://zh.visualgo.net/sorting
若是手動比較難以理解,可參照以上連接的可視化過程來觀察。

擴展:設計算法,對於給定的介於0--k之間的n個整數進行預處理,並在O(1)時間內獲得這n個整數有多少落在了(a,b]區間內。以上算法便可用來處理,預處理的時間爲O(n+k)。

  • 用計數排序中的預處理方法,預處理輔助數組,使得temp[i]爲不大於i的元素的個數。

  • (a,b]區間內元素個數即爲temp[b] - temp[a]

/**
 * 計數排序的擴展
 */
public class CountSortExt {
    private int[] temp;        // 輔助數組
    public CountSortExt(int[] a){
        int max = max(a);
        temp = new int[max+1];
        for(int i = 0; i<a.length; i++)
            temp[a[i]] += 1;
        for(int i = 1; i<temp.length; i++)
            temp[i] += temp[i-1];
    }
    private int max(int[] a) {
        int max = a[0];
        for(int cur: a)
            if(max < cur)    max = cur;
        return max;
    }
    /**返回(a,b]之間元素的個數*/
    public int getCountBetweenAandB(int a, int b){
        return temp[b] - temp[a];
    }
    public static void main(String[] args) {
        int[] arr = {1,2,2,3,2,8,0};
        CountSortExt e = new CountSortExt(arr);
        System.out.println(e.getCountBetweenAandB(1, 8));
    }
}

結果爲:
5


8.桶排序

參考http://www.growingwiththeweb....
使用場景:輸入的待排序數組在一個範圍內均勻分佈。
複雜度:
圖片描述

何時是最好狀況呢?

  • O(n+k)的額外空間不是個事兒。

  • 上面說到的使用場景:輸入數組在一個範圍內均勻分佈。
    那麼何時是最壞呢?

  • 數組的全部元素都進入同一個桶。

/**
 * 桶排序
 */
public class BucketSort {
    private static final int DEFAULT_BUCKET_SIZE = 5;
    public static void sort(Integer[] array){
        sort(array, DEFAULT_BUCKET_SIZE);
    }
    public static void sort(Integer[] array, int size) {
        if(array == null || array.length == 0)    return;
        // 找最大最小值
        int min = array[0], max = array[0];
        for(int i=1; i<array.length; i++){
            if(array[i]<min)        min = array[i];
            else if(array[i] > max)    max = array[i];
        }
        
        // 初始化桶
        int bucketCount = (max - min) / size + 1;
        List<List<Integer>> buckets = new ArrayList<>(bucketCount);
        for(int i = 0; i < bucketCount; i++)
            buckets.add(new ArrayList<Integer>());
        
        // 把輸入數組均勻分佈進buckets
        for(int i = 0; i<array.length; i++){
            int current = array[i];
            int index = (current - min) / size;
            buckets.get(index).add(current);
        }
        
        // 對每一個桶進行排序,而且每一個桶中的數據放置回數組
        int currentIndex = 0;
        for(int i = 0; i < buckets.size(); i++){
            List<Integer> currentBucket = buckets.get(i);
            Integer[] bucketArray = new Integer[currentBucket.size()];
            bucketArray = currentBucket.toArray(bucketArray);
            Arrays.sort(bucketArray);
            for(int j = 0; j< bucketArray.length; j++)
                array[currentIndex++] = bucketArray[j];
        }
    }
    public static void main(String[] args) {
        Integer[] array = {3,213,3,4,5,32,3,88,10};
        sort(array);
        System.out.println(Arrays.toString(array));
    }
}
[3, 3, 3, 4, 5, 10, 32, 88, 213]

9.基數排序

非比較型整數排序算法,原理是將整數按位切割成不一樣數字,而後按每一個位數分別比較。因爲整數也能夠表達字符串(好比名字或日期)和特定格式的浮點數,因此基數排序也不是隻能適用於整數。
實現:將全部待比較數值(正整數)統一爲一樣的數位長度,數位較短的數前面補零,而後從最低位開始,依次進行一次排序,這樣從最低位排序一直到最高位排序完成後,數列就變成有序的。
實現參考連接:
http://www.growingwiththeweb....
該基數排序基於LSD(Least significant digit),從最低有效關鍵字開始排序。首先對全部的數據按照次要關鍵字排序,而後對全部的數據按照首要關鍵字排序。

圖片描述

/**
 * 基數排序
 */
public class RadixSort {
    public static void sort(Integer[] array){
        sort(array, 10);
    }

    private static void sort(Integer[] array, int radix) {
        if(array == null || array.length == 0)    return;
        // 找最大最小值
        int min = array[0], max = array[0];
        for(int i = 1; i<array.length; i++){
            if(array[i] < min)        min = array[i];
            else if(array[i] > max)    max = array[i];
        }
        
        
        int exponent = 1;
        int off = max - min;
        // 對每一位進行計數排序
        while(off / exponent >= 1){
            countingSortByDigit(array, radix, exponent, min);
            exponent *= radix;
        }
    }

    private static void countingSortByDigit(Integer[] array, int radix, int exponent, int min) {
        int bucketIndex;
        int[] buckets = new int[radix];
        int[] output = new int[array.length];
        // 初始化桶
        for(int i=0; i<radix; i++)
            buckets[i] = 0;
        // 統計頻率
        for(int i = 0; i<array.length; i++){
            bucketIndex = (int)(((array[i] - min) / exponent) % radix);
            buckets[bucketIndex]++;
        }
        // 統計
        for(int i = 1; i< radix; i++)
            buckets[i] += buckets[i-1];
        // 移動記錄
        for(int i = array.length - 1; i>=0; i--){
            bucketIndex = (int)(((array[i] - min) / exponent) % radix);
            output[--buckets[bucketIndex]] = array[i];
        }
        // 拷貝回去
        for(int i =0; i<array.length;i++){
            array[i] = output[i];
        }
    }
    public static void main(String[] args) {
        Integer[] array = {312,213,43,4,52,32,3,88,101};
        sort(array);
        System.out.println(Arrays.toString(array));
    }
}

先總結到這裏。

相關文章
相關標籤/搜索