1. 程式人生 > 實用技巧 >Java——歸併排序

Java——歸併排序

歸併排序

  • 歸併排序是建立在歸併操作上的一種有效的排序演算法,該演算法是採用分治法的一個非常典型的應用。將已有序的子序列合併,得到完全有序的序列;即先使每個子序列有序,再使子序列段間有序。若將兩個有序表合併成一個有序表,稱為二路歸併。

需求:

  • 排序前:{8,4,5,7,1,3,6,2}
  • 排序後:{1,2,3,4,5,6,7,8}

排序原理:

1.儘可能的一組資料拆分成兩個元素相等的子組,並對每一個子組繼續拆分,直到拆分後的每個子組的元素個數是1為止。

2.將相鄰的兩個子組進行合併成一個有序的大組;

3.不斷的重複步驟2,直到最終只有一個組為止。

歸併排序API設計:

類名 Merge
構造方法 Merge():建立Merge物件
成員方法 1.public static void sort(Comparable[] a):對陣列內的元素進行排序
2.private static void sort(Comparable[] a, int lo, int hi):對陣列a中從索引lo到索引hi之間的元素進行排序
3.private static void merge(Comparable[] a, int lo, int mid, int hi):從索引lo到所以mid為一個子組,從索引mid+1到索引hi為另一個子組,把陣列a中的這兩個子組的資料合併成一個有序的大組(從索引lo到索引hi)
4.private static boolean less(Comparable v,Comparable w):判斷v是否小於w
5.private static void exch(Comparable[] a,int i,int j):交換a陣列中,索引i和索引j處的值
成員變數 1.private static Comparable[] assist:完成歸併操作需要的輔助陣列

歸併原理

歸併排序的程式碼實現:

// 歸併排序
public class Merge {
    //歸併所需要的輔助陣列
    private static Comparable[] assist;

    /*
       比較v元素是否小於w元素
    */
    private static boolean less(Comparable v, Comparable w) {
        return v.compareTo(w) < 0;
    }

    /*
    陣列元素i和j交換位置
     */
    private static void exch(Comparable[] a, int i, int j) {
        Comparable t = a[i];
        a[i] = a[j];
        a[j] = t;
    }


    /*
           對陣列a中的元素進行排序
        */
    public static void sort(Comparable[] a) {
        //1.初始化輔助陣列assist;
        assist = new Comparable[a.length];
        //2.定義一個lo變數,和hi變數,分別記錄陣列中最小的索引和最大的索引;
        int lo = 0;
        int hi = a.length - 1;
        //3.呼叫sort過載方法完成陣列a中,從索引lo到索引hi的元素的排序
        sort(a, lo, hi);
    }

    /*
    對陣列a中從lo到hi的元素進行排序
     */
    private static void sort(Comparable[] a, int lo, int hi) {
        //做安全性校驗;
        if (hi <= lo) {
            return;
        }

        //對lo到hi之間的資料進行分為兩個組
        int mid = lo + (hi - lo) / 2;//   5,9  mid=7

        //分別對每一組資料進行排序
        sort(a, lo, mid);
        sort(a, mid + 1, hi);

        //再把兩個組中的資料進行歸併
        merge(a, lo, mid, hi);
    }

    /*
    對陣列中,從lo到mid為一組,從mid+1到hi為一組,對這兩組資料進行歸併
     */
    private static void merge(Comparable[] a, int lo, int mid, int hi) {
        //定義三個指標
        int i = lo;
        int p1 = lo;
        int p2 = mid + 1;

        //遍歷,移動p1指標和p2指標,比較對應索引處的值,找出小的那個,放到輔助陣列的對應索引處
        while (p1 <= mid && p2 <= hi) {
            //比較對應索引處的值
            if (less(a[p1], a[p2])) {
                assist[i++] = a[p1++];
            } else {
                assist[i++] = a[p2++];
            }
        }

        //遍歷,如果p1的指標沒有走完,那麼順序移動p1指標,把對應的元素放到輔助陣列的對應索引處
        while (p1 <= mid) {
            assist[i++] = a[p1++];
        }
        //遍歷,如果p2的指標沒有走完,那麼順序移動p2指標,把對應的元素放到輔助陣列的對應索引處
        while (p2 <= hi) {
            assist[i++] = a[p2++];
        }
        //把輔助陣列中的元素拷貝到原陣列中
        for (int index = lo; index <= hi; index++) {
            a[index] = assist[index];
        }

    }

}

// 測試程式碼
public class MergeTest {

    public static void main(String[] args) {
        Integer[] a = {8, 4, 5, 7, 1, 3, 6, 2};
        Merge.sort(a);
        System.out.println(Arrays.toString(a));//{1,2,3,4,5,6,7,8}
    }
}


歸併排序的時間複雜度分析

  • 歸併排序是分治思想的最典型的例子,上面的演算法中,對a[lo...hi]進行排序,先將它分為a[lo...mid]和a[mid+1...hi]兩部分,分別通過遞迴呼叫將他們單獨排序,最後將有序的子陣列歸併為最終的排序結果。該遞迴的出口在於如果一個數組不能再被分為兩個子陣列,那麼就會執行merge進行歸併,在歸併的時候判斷元素的大小進行排序。

  • 用樹狀圖來描述歸併,如果一個數組有8個元素,那麼它將每次除以2找最小的子陣列,共拆log8次,值為3,所以樹共有3層,那麼自頂向下第k層有2k個子陣列,每個陣列的長度為2(3-k),歸併最多需要2^(3-k)次比較。因此每層的比較次數為 2^k * 2(3-k)=23,那麼3層總共為 3*2^3。
  • 假設元素的個數為n,那麼使用歸併排序拆分的次數為log2(n),所以共log2(n)層,那麼使用log2(n)替換上面32^3中 的3這個層數,最終得出的歸併排序的時間複雜度為:log2(n) 2^(log2(n))=log2(n)*n,根據大O推導法則,忽略底數,最終歸併排序的時間複雜度為O(nlogn);

歸併排序的缺點

  • 需要申請額外的陣列空間,導致空間複雜度提升,是典型的以空間換時間的操作。

歸併排序與希爾排序效能測試:

  • 之前我們通過測試可以知道希爾排序的效能是由於插入排序的,那現在學習了歸併排序後,歸併排序的效率與希爾排序的效率哪個高呢?我們使用同樣的測試方式來完成一樣這兩個排序演算法之間的效能比較
  • 在資料的測試資料資料夾下有一個reverse_arr.txt檔案,裡面存放的是從1000000到1的逆向資料,我們可以根據這個批量資料完成測試。測試的思想:在執行排序前前記錄一個時間,在排序完成後記錄一個時間,兩個時間的時間差就是排序的耗時。

歸併排序和希爾排序效能比較測試程式碼:

public class SortCompare {
    //呼叫不同的測試方法,完成測試
    public static void main(String[] args) throws Exception {
        //1.建立一個ArrayList集合,儲存讀取出來的整數
        ArrayList<Integer> list = new ArrayList<>();

        //2.建立快取讀取流BufferedReader,讀取資料,並存儲到ArrayList中;
        BufferedReader reader = new BufferedReader(new InputStreamReader(SortCompare.class.getClassLoader().getResourceAsStream("reverse_arr.txt")));
        String line = null;
        while ((line = reader.readLine()) != null) {
            //line是字串,把line轉換成Integer,儲存到集合中
            int i = Integer.parseInt(line);
            list.add(i);
        }

        reader.close();


        //3.把ArrayList集合轉換成陣列
        Integer[] a = new Integer[list.size()];
        list.toArray(a);
        //4.呼叫測試程式碼完成測試
//        testInsertion(a);//34929毫秒
//        testShell(a);//43毫秒
        testMerge(a);//97毫秒
        

    }

    //測試希爾排序
    private static void testShell(Integer[] a) {
        //1.獲取執行之前的時間
        long start = System.currentTimeMillis();
        //2.執行演算法程式碼
        Shell.sort(a);
        //3.獲取執行之後的時間
        long end = System.currentTimeMillis();
        //4.算出程式執行的時間並輸出
        System.out.println("希爾排序執行的時間為:" + (end - start) + "毫秒");

    }

    //測試插入排序
    private static void testInsertion(Integer[] a) {
        //1.獲取執行之前的時間
        long start = System.currentTimeMillis();
        //2.執行演算法程式碼
        Insertion.sort(a);
        //3.獲取執行之後的時間
        long end = System.currentTimeMillis();
        //4.算出程式執行的時間並輸出
        System.out.println("插入排序執行的時間為:" + (end - start) + "毫秒");
    }

    //測試歸併排序
    private static void testMerge(Integer[] a) {
        //1.獲取執行之前的時間
        long start = System.currentTimeMillis();
        //2.執行演算法程式碼
        Merge.sort(a);
        //3.獲取執行之後的時間
        long end = System.currentTimeMillis();
        //4.算出程式執行的時間並輸出
        System.out.println("歸併排序執行的時間為:" + (end - start) + "毫秒");
    }
}
  • 通過測試,發現希爾排序和歸併排序在處理大批量資料時差別不是很大。