1. 程式人生 > >【Algorithms公開課學習筆記5】排序演算法part2——歸併排序

【Algorithms公開課學習筆記5】排序演算法part2——歸併排序

Merge Sort 歸併排序

0.前言

前面的文章已經分析了選擇排序、插入排序、希爾排序等基礎排序演算法,本文將分析一個性能極高的排序演算法——歸併排序。在Java程式設計的時候,我們經常會使用到一個API:Arrays.sort(o),如果括號中的o是物件的話,那麼該API就是呼叫歸併排序演算法(結合了插入排序)來完成排序的。順帶一提,如果括號裡的o是java基本型別,那麼該API就會呼叫快速排序演算法。關於快速排序分析,將會在下一篇文章。

1.歸併排序

原理

歸併排序運用了遞迴的思想:


input:陣列s

sort(陣列s):
    1.將輸入的陣列s對半分成兩部分
    2.
對左右兩部分(遞迴地)呼叫sort() 3.對左右兩部分呼叫merge()進行合併 merge(): 1.確定需要合併的兩部分陣列有序,否則不能合併 2.複製一個輔助陣列aux,存放原陣列 3.合併陣列的操作就是將這兩部分陣列按序擺放後存入原陣列,輔助陣列提供原位置參考(這裡可以使用插入排序演算法來排序)

程式碼實現


public class Merge{
    private static void merge(Comparable[] a, Comparable[] aux, int lo, int mid, int hi){
        //保證需要合併的兩部分是有序的
assert isSorted(a, lo, mid); assert isSorted(a, mid+1, hi); //賦值輔助陣列 for (int k = lo; k <= hi; k++){ aux[k] = a[k]; } 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++]; } assert isSorted(a, 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); } //外部排序演算法 public static void sort(Comparable[] a){ //定義輔助陣列,千萬不要在遞迴演算法內定義 aux = new Comparable[a.length]; sort(a, aux, 0, a.length - 1); } }

注意事項:

  • 1.assert語句是用來測試程式中的假設條件的,如果假設條件不滿足true的話就會丟擲異常exception。執行時是預設關閉的,可以通過設定開啟。
  • 2.輔助陣列千萬不能在遞迴演算法內定義,否則會極大的浪費空間

時間效能

歸併排序中最多有NlgN次比較和6NlgN次陣列訪問。以下通過數學來分析一下NlgN次比較值的由來:

將比較compare的次數記C(N), C(N)<=C(N/2)+C(N/2)+N, 假設N是2的指數 那麼D(N)<=2C(N/2)+N


D(N) /N = 2D(N/2) /N + 1
= D(N/2) /(N/2) + 1
= D(N/4) /(N/4) + 1 + 1
= D(N/8) /(N/8) + 1 + 1 + 1
. . .
= D(N/N) /(N/N) + 1 + 1 + ... + 1
= lgN

D(N) = NlgN

另外,通過樹結構來分析也可以得出此結論:

空間佔用率

歸併排序將要利用額外的輔助陣列來實現合併,而輔助陣列大小需要為N

優化

優化一:當陣列被分割到很小的時候(臨界值),改用插入排序演算法來對其進行排序。這個臨界值可以根據實際情況來確定。


private static void sort(Comparable[] a, Comparable[] aux, int lo, int hi){
    //臨界值命名為CUTOFF
    if (hi <= lo + CUTOFF - 1){
        Insertion.sort(a, lo, hi);
        return;
    }
    int mid = lo + (hi - lo) / 2;
    sort (a, aux, lo, mid);
    sort (a, aux, mid+1, hi);
    merge(a, aux, lo, mid, 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);
    //判斷合併前是否已經全有序
    if (!less(a[mid+1], a[mid])) return;
    merge(a, aux, lo, mid, hi);
}

優化三:去掉對輔助陣列複製賦值的程式碼,這並不能節約空間,但可以節省時間。(比較難理解)

2.自底向上的歸併排序

原理

自底向上的歸併排序沒有使用遞迴,而是根據一個從小到大(通常從個開始)序列來不斷合併陣列:


input:陣列s

sort(陣列s):
    1.初始合併規模sz=1
    2.合併sz的子陣列,形成sz*2的陣列
    3.sz=sz*2
    4.loop

merge():
    1.確定需要合併的兩部分陣列有序,否則不能合併
    2.複製一個輔助陣列aux,存放原陣列
    3.合併陣列的操作就是將這兩部分陣列按序擺放後存入原陣列,輔助陣列提供原位置參考(這裡可以使用插入排序演算法來排序)

程式碼實現


public class MergeBU{
    private static void merge(Comparable[] a, Comparable[] aux, int lo, int mid, int hi){
        //保證需要合併的兩部分是有序的
        assert isSorted(a, lo, mid);
        assert isSorted(a, mid+1, hi);
        //賦值輔助陣列
        for (int k = lo; k <= hi; k++){
            aux[k] = a[k];
        }
        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++];
        }
        assert isSorted(a, lo, hi);
    }

    public static void sort(Comparable[] a){
        int N = a.length;
        Comparable[] aux = new Comparable[N];
        //初始sz=1
        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));
    }
}

時間效能

歸併排序中最多有NlgN次比較和6NlgN次陣列訪問。但實際實驗測試的時間比傳統的歸併排序演算法多10%。

空間佔用率

歸併排序將要利用額外的輔助陣列來實現合併,而輔助陣列大小需要為N

3.Comparator inteface

上一篇文章提到了Comparable介面,以及實現該介面必須重寫的compareTo()方法。傳送門 實現Comparable介面的類物件可以在compareTo()方法內自定義比較規則; 而實現Comparator介面的類物件則可以在重寫的compare()方法內自定義排序規則,具體如下:

//定義的時候
private class SlopComparator implements Comparator<Point> {
    @Override
    public int compare(Point o1, Point o2) {
        double slop1 = slopeTo(o1);
        double slop2 = slopeTo(o2);
        if (slop1 > slop2) {
            return 1;
        } else if (slop1 < slop2) {
            return -1;
        } else {
            return 0;
        }
    }
}

// 實現的時候
// 對陣列p,按照對p[0]的斜率排序
Arrays.sort(p, p[0].slopeOrder());

4.穩定性stability

穩定性stability是排序演算法在實踐中的一個重要指標。所謂的穩定性是指陣列的相同值在排序的過程中會保持其位置的相對性。下圖所示就是具有相對性排序結果:

判斷相對性的一個重要依據就是:值在一次交換的過程中是否會跨過與其相同的值,即是否存在長距離的交換。 在插入排序中,每次交換都是相鄰的值之間的交換,所以不會跨過與其相同的值,因此是穩定的排序演算法。 在選擇排序中,值交換都是直接放在其最終位置上的,所以很可能會跨過與其相同的值,因此不是穩定的排序演算法。

在目前已經學習過的演算法中, 穩定的排序演算法:插入排序,歸併排序 不穩定的排序演算法:選擇排序,希爾排序,快速排序