排序算法系列:歸併排序算法

概述

上一篇我們說了一個非常簡單的排序算法——選擇排序。其複雜程序完全是冒泡級的,甚至比冒泡還要簡單。今天要說的是一個相對比較複雜的排序算法——歸併排序。複雜的原因不僅在於歸併排序分成了兩個部分進行解決問題,而是在於,你需要一些算法的思想支撐。
歸併排序和之前我寫的一篇博客《大數據算法:對5億數據進行排序》有很多相似的地方,如果你感興趣,也可以去看看那一篇博客。


版權說明

著作權歸作者所有。
商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。
本文作者:Q-WHai
發表日期: 2016年5月27日
本文鏈接:http://blog.csdn.net/lemon_tree12138/article/details/51517753
來源:CSDN
更多內容:分類 >> 算法與數學


目錄


弱分治歸併

歸併的核心算法就是上面提到過的兩個過程。分別是分治與合併。合併都好理解,那麼什麼是分治呢?下面就來逐一說明一下。

算法原理

弱分治歸併排序算法中,我們主要說的是合併,因爲這裏的分治更像是分組。

背景

假設我們有序列 T0 = [ 4, 3, 6, 5, 9, 0, 8, 1, 7, 2 ]
那麼,在一開始,我們的序列就被分成了 10 組,每一組的元素個數爲 1。

合併

先說合並吧,因爲它簡單一些。在合併模塊中,需要傳入兩個序列參數,並保證待合併的兩個序列本身已經有序。現在我們假設待合併的兩個有序序列分別爲:
t0 = [ 0, 9 ]
t1 = [ 1, 8 ]
看到這兩個序列讓人很自然地想到,只要依次取 t0 和 t1 中的最小的元素即可。並且最小的元素就是第一個元素。當我們取完 t0 中的 0 之後,t0 中就不再有 0 了,這一點很重要。表現在代碼上就是下標的移動了。第二次取到的是 t1 中的 1。重複這個過程,就可以獲得合併後的有序序列 tm = [ 0, 1, 8, 9, ]。
合併過程圖解

這裏寫圖片描述

下面是合併的核心代碼

// 合併的核心模塊
    private void merge(int[] array, int low, int mid, int hight) {
        if (low >= hight) {
            return;
        }

        int[] auxArray = new int[hight - low + 1];
        int index1 = low;
        int index2 = mid + 1;

        int i = 0;
        while(index1 <= mid && index2 <= hight) {
            if (array[index1] <= array[index2]) {
                auxArray[i] = array[index1];
                index1++;
                i++;
            } else {
                auxArray[i] = array[index2];
                index2++;
                i++;
            }
        }

        // 繼續合併前半段數組中未被合併的部分
        while (index1 <= mid) {
            auxArray[i] = array[index1];
            index1++;
            i++;
        }

        // 繼續合併後半段數組中未被合併的部分
        while (index2 <= hight) {
            auxArray[i] = array[index2];
            index2++;
            i++;
        }

        // 將合併好的序列寫回到數組中
        for (int j = 0; j < auxArray.length; j++) {
            array[low + j] = auxArray[j];
        }
    }

分治

我想大部分人應該不會被合併邏輯給難住吧。只是分治的邏輯會有一些麻煩,麻煩不是在於分治思想的麻煩,而是分治過程的邏輯代碼不好編寫。正因爲如此,所以我們在前面先講解弱分治歸併,這樣在下面看到強分治歸併的分治邏輯時,你纔不會毫無頭緒。在上面也說了,弱分治並歸更像是一個分組合並的過程。也就是一開始就有很多組,然後慢慢合併,在合併的過程中分組減少了,合併後的有序數組變大了,直至只有一個數組爲止。
在合併中最容易想到的是兩兩合併。所以在分組後,就兩兩進行合併。只要我們能準確地取到相鄰的兩個序列就可以進行合併了。
下面是代碼實現

// 對數組進行分組的核心模塊
    private void sortCore(int[] array) {
        int length = array.length;

        int groupSize = 1;
        while(groupSize < length) {
            for (int i = 0; i < length; i += (groupSize * 2)) {
                int low = i;
                int hight = Math.min(i + groupSize * 2 - 1, length - 1);
                int middle = low + groupSize - 1;
                merge(array, low, middle >= hight ? (low + hight) / 2 : middle, hight);
            }
            groupSize *= 2;
        }

        // 對分組中的奇數情況進行另外處理
        if (groupSize / 2 < length) {
            int low = 0;
            int hight = length - 1;
            merge(array, low, groupSize / 2 - 1, hight);
        }
    }

在上面的代碼中,可以看到最後有一個奇數分組的邏輯處理。這是怎麼回事呢?很好理解,假設,現在給你 (2n + 1) 個分組的有序序列,按照前面講的兩兩合併,那麼只能合併前面的 2n 個序列,第 (2n + 1) 個序列找到可合併的對象。處理的方式就是把它保留到最後與迭代後的有序序列進行合併即可。這一點從下面的圖解中也可以獲知。

排序過程圖解

這裏寫圖片描述


算法實現

/**
 * <p>
 * 歸併排序算法
 * </p>
 * 2016年1月20日
 * 
 * @author <a href="http://weibo.com/u/5131020927">Q-WHai</a>
 * @see <a href="http://blog.csdn.net/lemon_tree12138">http://blog.csdn.net/lemon_tree12138</a>
 * @version 0.1.1
 */
public class MergeSort implements Sortable {

    @Override
    public int[] sort(int[] array) {
        if (array == null) {
            return null;
        }

        sortCore(array);

        return array;
    }

    // 對數組進行分組的核心模塊
    private void sortCore(int[] array) {
        ( ... 此處省略上面分治的邏輯 ... )
    }

    // 合併的核心模塊
    private void merge(int[] array, int low, int mid, int hight) {
        ( ... 此處省略上面合併的邏輯 ... )
    }
}

強分治歸併

算法原理

強分治歸併相比弱分治歸併的不同點在於,強分治歸併有沒在一開始就對數組 T0 進行分組,而是通過程序來對 T0 進行分組,現在可以看一張強分治歸併排序算法的過程圖感受一下。

這裏寫圖片描述

合併

不管弱分治歸併還是強分治歸併,其合併的邏輯都是一樣的。大家可以自行參考上面的邏輯,這裏就不廢話了。

分治

從上面的排序過程圖中也可以發現,強分治歸併需要將原數組先劃分成小數組。首先將一個大數組分割成兩個小數組,再將兩個小數組分割成四個小數組,如此往復。字裏行間都表明了,這裏需要進行遞歸操作。
強分治歸併算法的分治部分邏輯代碼:

/**
     * 對數組進行分組的核心模塊
     * 
     * @param array
     *      待排序數組
     * @param start
     *      開始位置
     * @param end
     *      結束位置(end 爲 數組可達下標)
     */
    private void sortCore(int[] array, int start, int end) {
        if (start == end) {
            return;
        } else {
            int middle = (start + end) / 2;
            sortCore(array, start, middle);
            sortCore(array, middle + 1, end);
            merge(array, start, middle, end);
        }
    }

總結

算法複雜度

排序方法 時間複雜度 空間複雜度 穩定性 複雜性
平均情況 最壞情況 最好情況
歸併排序 O(nlog n ) O(nlog n ) O(nlog n ) O(n+log n ) 穩定 較複雜

強弱分治歸併的比較

需要比較的主是代碼邏輯複雜度和運行效率
這裏我們用了一個數組爲: int[] array = { 4, 3, 6, 5, 9, 0, 8, 1, 7, 2 };
循環運行 1000000 次後得到如下結果:

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
MergeSort 用時:509 ms
-------------------------------------
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
MergeImproveSort 用時:374 ms
算法名稱 代碼邏輯複雜度 運行效率 bigger 值
弱分治歸併 簡單
強分治歸併 複雜

所以,如果想要運行效率高一些或是刷刷 bigger 值,那麼請使用強分治歸併排序算法。


Ref

  • 《大話數據結構》

Github源碼下載


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章