1. 程式人生 > >排序算法系列:歸併排序演算法

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

概述

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

版權說明

目錄

弱分治歸併

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

演算法原理

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

背景

假設我們有序列 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(nlogn) O(nlogn) O(nlogn) O(n+logn) 穩定 較複雜

強弱分治歸併的比較

需要比較的主是程式碼邏輯複雜度和執行效率
這裡我們用了一個數組為: 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原始碼下載