都2020年了,據說你還不會歸併排序?手把手教你手寫歸併排序算法

本文介紹了歸併排序的基本思想,遞歸方法的通常寫法,最後一步步手寫歸併排序,並對其性能進行了分析。java

基本思想

歸併排序是創建在歸併操做上的一種有效的排序算法,該算法是採用分治法的一個很是典型的應用。即先使每一個子序列有序,再將已有序的子序列合併,獲得徹底有序的序列。這裏給出一種遞歸形式的歸併排序實現。git

遞歸方法的通常寫法

遞歸方法的書寫主要有三步:github

  1. 明確遞歸方法的功能邊界;
  2. 獲得遞歸的遞推關係;
  3. 給定遞歸的終止條件。

遞歸方法都可按照這三步進行,切忌不要陷入遞歸實現的細節中。下面以歸併排序算法的書寫爲例,來談一下遞歸方法的具體寫法。算法

手寫歸併排序

首先,明確遞歸方法的功能,這裏咱們定義方法的功能爲,給定一個數組及左右邊界,方法完成數組邊界內元素的排序,以下:編程

private static void mergeSort(int[] arr,int left,int right);

先假設咱們已經有了這麼一個方法,不用管具體的實現。
接着,尋找遞推關係,什麼是遞推關係呢?就是如何由子問題的求解,來獲得原問題的求解,仍是舉例說明,有以下的數組
原始數組
咱們將其拆分爲左右兩部分,以下
拆分數組
遞推關係就是,假如左右兩部分都已經有序了,如何使整個數組有序?這個問題其實就是給定了一個數組,數組的左半部分有序,右半部分也有序,如何使整個數組有序?
首先,定義兩個指針,分別指向左側部分起始位置和右側部分起始位置,同時建立一個輔助數組和指向其初始位置的輔助指針
定義指針及輔助數組
接着比較,左指針和右指針所對應的元素的大小,較小的元素填充至輔助數組,同時其對應的指針和輔助指針均加1,以下:
比較並填充輔助數組
依次進行,直至某左指針指向中間位置或者右指針指向數組的末尾,此時要將將剩餘的元素填充至輔助數組。全部的元素填充完成後,再將輔助數組中的元素填充回原數組便可。具體的代碼以下:數組

/**
     *
     * @param arr 要合併的數組
     * @param left 左邊界
     * @param mid 中間的分界
     * @param right 右邊界
     */
    private static void merge(int[] arr,int left,int mid,int right){
        int[] helpArr = new int[right - left + 1];//首先定義一個輔助數組
        int lPoint = left;//左指針
        int rPoint = mid  + 1;//右指針
        int i = 0;//輔助指針
        while(lPoint <= mid && rPoint <= right){//比較並填充輔助數組
            if(arr[lPoint] <=  arr[rPoint])
                helpArr[i++] =  arr[lPoint++];
            else
                helpArr[i++] =  arr[rPoint++];
        }
        while(lPoint <= mid){//將剩餘元素填充至輔助數組
            helpArr[i++] =  arr[lPoint++];
        }
        while(rPoint <= right){
            helpArr[i++] =  arr[rPoint++];
        }
        for(int j = 0;j < helpArr.length;j ++){//將輔助數組中的元素回填至原數組
            arr[left + j] = helpArr[j];
        }
    }

最後,肯定終止條件,通常是數組爲空或者數組中只有一個元素,返回便可。
如今咱們能夠寫出整個歸併排序的代碼了,以下:數據結構

private static void mergeSort(int[] arr,int left,int right){
        if(arr == null || right == left)//終止條件
            return ;
        int mid = left + (right - left) / 2;//肯定分割的邊界
        mergeSort(arr,left,mid);//對左半部分調用遞歸方法,使其有序
        mergeSort(arr,mid + 1,right);//對右半部分調用遞歸方法,使其有序
        merge(arr,left,mid,right);//合併左右兩部分,使整個數組有序
    }

爲了保證形式的統一,再對函數進行一下封裝,以下,這就是咱們的歸併排序了。函數

/**
     * 歸併排序算法
     * @param arr
     */
    public static void mergeSort(int[] arr){
        mergeSort(arr,0,arr.length - 1);//調用寫好的遞歸版歸併排序方法
    }

至此,咱們便完成了歸併排序算法的代碼實現。性能

性能分析

在分析歸併排序算法性能以前,先介紹幾個基礎的概念。
時間複雜度:一個算法執行所消耗的時間;
空間複雜度:運行完一個算法所需的內存大小;
原地排序:在排序過程當中不申請多餘的存儲空間,只利用原來存儲待排數據的存儲空間進行比較和交換的數據排序。
非原地排序:須要利用額外的數組來輔助排序。
穩定排序:若是 a 本來在 b 的前面,且 a == b,排序以後 a 仍然在 b 的前面,則爲穩定排序。
非穩定排序:若是 a 本來在 b 的前面,且 a == b,排序以後 a 可能不在 b 的前面,則爲非穩定排序。
下面咱們分析下歸併排序算法的性能。
首先是時間複雜度。歸併排序算法在排序時首先將問題進行分解,而後解決子問題,再合併,因此總時間=分解時間+解決子問題時間+合併時間。分解時間就是把一個數組分解爲左右兩部分,時間爲一常數,即O(1);解決子問題時間是兩個遞歸方法,把一個規模爲n的問題分紅兩個規模分別爲n/2的子問題,時間爲2T(n/2);合併時間複雜度爲O(n)。因此總時間T(n)=2T(n/2)+O(n)。這個遞歸問題的時間複雜度能夠用下面的公式來計算
遞歸函數的時間複雜度計算公式
這個公式可針對形如:T(n) = aT(n/b) + f(n)的遞歸方程進行時間複雜度求解。帶入可知,歸併排序的時間複雜度爲O(nlogn)。此外在最壞、最佳、平均狀況下歸併排序時間複雜度均爲O(nlogn)。
空間複雜度分析:在排序過程當中使用了一個與原數組等長的輔助數組,估空間複雜度爲O(n)。
穩定性分析:由排序過程能夠知道,歸併排序是一種穩定排序。
是否原地排序:排序過程當中用到了輔助數組,因此是非原地排序。
本文源碼已同步至github,地址:https://github.com/zhanglianchao/AllForJava/tree/master/src/algorithm/sort指針

以爲文章有用的話,點贊+關注唄,好讓更多的人看到這篇文章,也激勵博主寫出更多的好文章。
更多關於算法、數據結構和計算機基礎知識的內容,歡迎掃碼關注個人原創公衆號「超悅編程」。

超悅編程

相關文章
相關標籤/搜索