幾種常見排序算法

幾種常見排序算法

標籤: algorithmshtml


[TOC]java


本文介紹幾種常見排序算法(選擇排序,插入排序,希爾排序,歸併排序,快速排序,堆排序),對算法的思路、性質、特色、具體步驟、java實現以及trace圖解進行了全面的說明。最後對幾種排序算法進行了比較和總結。git

寫在前面

基礎介紹

java: Interface Comparable<T>github

Java中不少類已經實現了Comparable<T>接口,用戶也可自定義類型實現該接口算法

total order:api

  • Antisymmetry(反對稱性): if v ≤ w and w ≤ v, then v = w.
  • Transitivity(傳遞性): if v ≤ w and w ≤ x, then v ≤ x.
  • Totality: either v ≤ w or w ≤ v or both.

注意: The <= operator for double is not a total order,violates totality: (Double.NaN <= Double.NaN) is false數組

通用代碼:oracle

// Less. Is item v less than w ?
private static boolean less(Comparable v, Comparable w) {
    return v.compareTo(w) < 0;
}

//Exchange. Swap item in array a[] at index i with the one at index j
private static void exch(Comparable[] a,, int i, int j) {
    Comparable swap = a[i];
    a[i] = a[j];
    a[j] = swap;
}

初級排序算法

selection sort(選擇排序)

思路:less

  • 在第i次迭代中,在剩下的(即未排序的)元素中找到最小的元素
  • 將第i個元素與最小的元素交換位置

現象:dom

  • 設已排序的和未排序的交界處爲 ↑,則每次循環, ↑ 從左往右移動一個位置
  • ↑ 左邊的元素(包括↑)固定了,且升序
  • ↑ 右邊的任一元素所有比左邊的全部元素都大

選擇排序

步驟:

  • move the pointer to the right
  • indentify index of minimun entry on right
  • exchange into positon

選擇排序

java實現:

public static void sort(Comparable[] a) {
    int N = a.length;
    for (int i = 0; i < N; i++) {
        int min = i;
        for (int j = i+1; j < N; j++) {
            if (less(a[j], a[min])) min = j;
        }
        exch(a, i, min);
    }
}

特色:

  • 運行時間和輸入無關,不管輸入是已排序,時間複雜度都是O(n^2)
  • 數據移動最少,交換的次數和數組大小是線性關係

insertion sort(插入排序)

思路:

  • 在第i次迭代中,將第i個元素與每個它左邊且比它大的的元素交換位置

現象:

  • 設已排序的和未排序的交界處爲 ↑,則每次循環, ↑ 從左往右移動一個位置
  • ↑ 左邊的元素(包括↑)且升序,但位置不固定(由於後續可能會因插入而移動)
  • ↑ 右邊的元素還不可見

插入排序

步驟:

  • Move the pointer to the right.
  • Moving from right to left, exchange a[i] with each larger entry to its left.

插入排序

java實現:

public static void sort(Comparable[] a) {
    int N = a.length;
    for (int i = 0; i < N; i++) {
        for (int j = i; j > 0 && less(a[j], a[j-1]); j--) {
            exch(a, j, j-1);
        }
    }
}

inversion(倒置):An inversion is a pair of keys that are out of order

部分有序:An array is partially sorted if the number of inversions is ≤ c N.

特色:

  • 運行時間和輸入有關,當輸入已排序時,時間複雜度是O(n);
  • For partially-sorted arrays, insertion sort runs in linear time.(交換的次數等於輸入中倒置(inversion)的個數)
  • 插入排序適合部分有序數組,也適合小規模數組

ShellSort(希爾排序)

希爾排序是基於插入排序的。

思路:

  • Move entries more than one position at a time by h-sorting the array
  • 按照h的步長進行插入排序

現象:

  • 數組中任意間隔爲h的元素都是有序的
  • A g-sorted array remains g-sorted after h-sorting it.

希爾排序

性質:

  • 遞增數列通常採用3x+1:1,4,13,40,121,364.....,使用這種遞增數列的希爾排序所需的比較次數不會超過N的若干倍乘以遞增數列的長度。
  • 最壞狀況下,使用3x+1遞增數列的希爾排序的比較次數是O(N^(3/2))

java實現:

public static void sort(Comparable[] a) {
    int N = a.length;

    // 3x+1 increment sequence:  1, 4, 13, 40, 121, 364, 1093, ... 
    int h = 1;
    while (h < N/3) h = 3*h + 1; 

    while (h >= 1) {
        // h-sort the array
        for (int i = h; i < N; i++) {
            for (int j = i; j >= h && less(a[j], a[j-h]); j -= h) {
                exch(a, j, j-h);
            }
        }
        h /= 3;
    }
}

shuffing(不是排序算法)

目標:Rearrange array so that result is a uniformly random permutation

shuffle sort思路

  • 爲數組的每個位置生成一個隨機實數
  • 排序這個生成的數組

Knuth shuffle demo

  • In iteration i, pick integer r between 0 and i uniformly at random.
  • Swap a[i] and a[r].

correct variant: between i and N – 1


  • Mergesort--Java sort for objects.
  • Quicksort--Java sort for primitive types.

下面看看這兩種排序算法

merge sort(歸併排序)

思路:

  • Divide array into two halves.
  • Recursively sort each half.
  • Merge two halves.

Abstract in-place merge(原地歸併的抽象方法)

Given two sorted subarrays a[lo] to a[mid] and a[mid+1] to a[hi],replace with sorted subarray a[lo] to a[hi]

步驟:

  • 先將全部元素複製到aux[]中,再歸併回a[]中。
  • 歸併時的四個判斷:
    • 左半邊用盡(取右半邊元素)
    • 右半邊用盡(取左半邊元素)
    • 右半邊的當前元素小於左半邊的當前元素(取右半邊的元素)
    • 右半邊的當前元素大於/等於左半邊的當前元素(取左半邊的元素)

merging java實現:

// stably merge a[lo .. mid] with a[mid+1 ..hi] using aux[lo .. hi]
private static void merge(Comparable[] a, Comparable[] aux, int lo, int mid, int hi) {
    // precondition: a[lo .. mid] and a[mid+1 .. hi] are sorted subarrays
 
    // copy to aux[]
    for (int k = lo; k <= hi; k++) {
        aux[k] = a[k]; 
    }

    // merge back to a[]
    int i = lo, j = mid+1;
    for (int k = lo; k <= hi; k++) {
        if      (i > mid)              a[k] = aux[j++];
        else if (j > hi)               a[k] = aux[i++];
        else if (less(aux[j], aux[i])) a[k] = aux[j++];
        else                           a[k] = aux[i++];
    }
}

Top-down mergesort(自頂向下的歸併排序)

mergesort java實現:

// mergesort a[lo..hi] using auxiliary array aux[lo..hi]
private static void sort(Comparable[] a, Comparable[] aux, int lo, int hi) {
    if (hi <= lo) return;
    int mid = lo + (hi - lo) / 2;
    sort(a, aux, lo, mid);  //將左邊排序
    sort(a, aux, mid + 1, hi);  //將右邊排序
    merge(a, aux, lo, mid, hi); //歸併結果
}

自頂向下的歸併排序的軌跡圖

歸併排序

由圖可知,原地歸併排序的大體趨勢是,先局部排序,再擴大規模;先左邊排序,再右邊排序;每次都是左邊一半局部排完且merge了,右邊一半纔開始從最局部的地方開始排序。

改進

  • 對小規模子數組使用插入排序
  • 測試數組是否已經有序(左邊最大<右邊最小時,直接返回)
  • 不將元素複製到輔助數組(節省時間而非空間)

Bottom-up mergesort(自底向上的歸併排序)

思路:

  • 先歸併微型數組,從兩兩歸併開始(每一個元素理解爲大小爲1的數組)
  • 重複上述步驟,逐步擴大歸併的規模,2,4,8.....

java實現:

public class MergeBU{
   private static void merge(...){ /* as before */ }
 
   public static void sort(Comparable[] a){
     int N = a.length;
     Comparable[] aux = new Comparable[N];
     for (int sz = 1; sz < N; sz = sz+sz)
     for (int lo = 0; lo < N-sz; lo += sz+sz)
     merge(a, aux, lo, lo+sz-1, Math.min(lo+sz+sz-1, N-1));
   }
}

自底向上的歸併排序的軌跡圖

歸併排序

由圖可知,自底向上歸併排序的大體趨勢是,先局部排序,逐步擴大到全局排序;步調均勻,穩步擴大


quicksort

思路:

  • Shuffle the array.
  • Partition(切分) so that, for some j - entry a[j] is in place - no larger entry to the left of j - no smaller entry to the right of j
  • Sort each piece recursively.

其中很重要的一步就是Partition(切分),這個過程使得知足如下三個條件:

  • 對於某個j,a[j]已經排定;
  • a[lo]到a[j-1]中的全部元素都不大於a[j];
  • a[j+1]到a[hi]中的全部元素都不小於a[j];

partition java實現

// partition the subarray a[lo..hi] so that a[lo..j-1] <= a[j] <= a[j+1..hi]
// and return the index j.
private static int partition(Comparable[] a, int lo, int hi) {
    int i = lo;
    int j = hi + 1;
    Comparable v = a[lo];
    while (true) { 

        // find item on lo to swap
        while (less(a[++i], v))
            if (i == hi) break;

        // find item on hi to swap
        while (less(v, a[--j]))
            if (j == lo) break;      // redundant since a[lo] acts as sentinel

        // check if pointers cross
        if (i >= j) break;

        exch(a, i, j);
    }

    // put partitioning item v at a[j]
    exch(a, lo, j);

    // now, a[lo .. j-1] <= a[j] <= a[j+1 .. hi]
    return j;
}

快排java實現:

public static void sort(Comparable[] a) {
    StdRandom.shuffle(a);
    sort(a, 0, a.length - 1);
}

// quicksort the subarray from a[lo] to a[hi]
private static void sort(Comparable[] a, int lo, int hi) { 
    if (hi <= lo) return;
    int j = partition(a, lo, hi);
    sort(a, lo, j-1);
    sort(a, j+1, hi);
    assert isSorted(a, lo, hi);
}

快排的軌跡圖

快速排序

由圖可知,和歸併排序不一樣,快排的大體趨勢是,先全局大致有個走勢——左邊比右邊小,逐步細化到局部;也是先左後右;局部完成時所有排序也就完成了。

一些實現的細節:

  • 原地切分:不使用輔助數組
  • 別越界:測試條件(j == lo)是冗餘的(a[lo]不可能比本身小);
  • 保持隨機性:初始時的隨機打亂跟重要
  • 終止循環
  • 處理切分元素值有重複的狀況:這裏可能出問題

性質:

  • 快排是in-place的
  • 快排不穩定

改進

  • 對小規模子數組使用插入排序
  • 三取樣切分

三向切分的快速排序

思路:

  • Let v be partitioning item a[lo].
  • Scan i from left to right.
  • (a[i] < v): exchange a[lt] with a[i]; increment both lt and i
  • (a[i] > v): exchange a[gt] with a[i]; decrement gt
  • (a[i] == v): increment i

主要是經過增長一個指針來實現的。普通的快拍只有lo和high兩個指針,故只能記錄大於(high右邊)和小於(lo左邊)兩個區間,等於只能併入其中一個;這裏增長了使用了lt,i,gt三個指針,從而達到記錄大於(gt右邊)、小於(lt左邊)和等於(lt和i之間)三個區間。

三切分的示意圖

三向切分

三向切分的java實現:

// quicksort the subarray a[lo .. hi] using 3-way partitioning
private static void sort(Comparable[] a, int lo, int hi) { 
    if (hi <= lo) return;
    int lt = lo, gt = hi;
    Comparable v = a[lo];
    int i = lo;
    while (i <= gt) {
        int cmp = a[i].compareTo(v);
        if      (cmp < 0) exch(a, lt++, i++);
        else if (cmp > 0) exch(a, i, gt--);
        else              i++;
    }

    // a[lo..lt-1] < v = a[lt..gt] < a[gt+1..hi]. 
    sort(a, lo, lt-1);
    sort(a, gt+1, hi);
}

Heapsort(堆排序)

思路:

  • Create max-heap with all N keys.
  • Repeatedly remove the maximum key.
  • swim:由下至上的堆有序化
  • sink:由上至下的對有序化

堆排序主要分爲兩個階段:

  1. 堆的構造
  2. 下沉排序

java實現以下:

public static void sort(Comparable[] pq) {
    int N = pq.length;
    //堆的構造
    for (int k = N/2; k >= 1; k--)
        sink(pq, k, N);
    
    //下沉排序
    while (N > 1) {
        exch(pq, 1, N--);
        sink(pq, 1, N);
    }
}

堆排序的軌跡圖

堆排序

由圖看出,堆排序的趨勢是,堆構造階段,大體是降序的走勢,到了下沉階段,從右到左(或者說從後往前)逐步有序

Significance: In-place sorting algorithm with N log N worst-case.

  • Mergesort: no, linear extra space.
  • Quicksort: no, quadratic time in worst case

缺點

  • Inner loop longer than quicksort’s.
  • Makes poor use of cache memory.
  • Not stable(不穩定)

總結和比較

排序算法總結表

總結

最好狀況和最壞狀況:參見上面的表格

關於穩定性:

  • 穩定性,插入排序,歸併排序
  • 不穩定:選擇排序,快排,希爾排序,堆排序
  • 緣由: Long-distance exchange

關於額外空間:除了歸併排序須要線性的額外空間,其餘都是in-place的

命題

  • 對於長度爲N的數組,選擇排序須要N^2/2次比較和N次交換(pf見P156)
  • 對於隨機排列的長度爲N的且主鍵不重複的數組(pf見P157)
    • 平均狀況下插入排序須要~N^2/4次比較和~N^2/4次交換
    • 最壞狀況下須要~N^2/2次比較和~N^2/2次交換,
    • 最好狀況下須要N-1次比較和0次交換。
  • Mergesort uses at most N lg N compares and 6 N lg N array accesses to sort any array of size N. (pf見P173)
  • Mergesort uses extra space proportional to N.(The array aux[] needs to be of size N for the last merge.)
  • Any compare-based sorting algorithm must use at least lg ( N ! ) ~ N lg N compares in the worst-case.(pf見P177)
  • 長度爲N的無重複數組排序,快速排序平均須要~2N ln N 次比較(以及1/6即1/3 N ln N的交換)
    • 最多須要約N^2/2次比較
    • 最少須要~N lg N 次比較
  • 用下沉操做由N個元素構造堆只需少於2N次比較以及少於N次交換(pf見P206)
  • 將N個元素排序,堆排序只需少於(2NlgN+2N)次比較以及一半次數的交換(pf見P208)

做者@brianway更多文章:我的網站 | CSDN | oschina

相關文章
相關標籤/搜索