#2 歸併排序算法的簡單分析

簡介

歸併排序是一種使用分治策略的排序算法,相比於以前介紹的插入排序算法,分治算法在數據量較大的場景中速度要快不少(在本文最後會比價兩個算法的優劣)ios

算法原理簡述

合併兩個已經排好序的數組

首先想象一下你的面前擺放着兩堆自上而下已經從大倒小排好序的撲克牌, 你如今要把這兩堆撲克牌按照從大到小的順序合併成一堆, 你須要先拿起牌堆一和牌堆二頂上的一張牌, 而後對比大小, 把小的哪一張放到手中, 大的哪一張放回到原來的牌堆, 重複上面的步驟, 直到其中一堆牌所有被放到手上後, 把另外一堆牌堆的全部牌依次追加到手中(說的有點繁瑣, 最好能本身按照上面的步驟試一下, 可以更快的理解).算法

圖解

歸併排序合併步驟.png

C++ 代碼

// 合併兩個排好序的子數組
// A: 處理的數組
// p: 子數組的起始
// q: 子數組的中點
// r: 子數組的結尾
void merge(int * A,int p, int q, int r){
    
    // 獲取左邊子數組的長度
    int n1 = q - p + 1;

    // 獲取右邊子數組的長度
    int n2 = r - q;
    
    // 建立一個新的數組保存左邊的子數組
    int * L = new int[n1];
    
    // 建立一個新的數組保存右邊的子數組
    int * R = new int[n2];
    
    // 保存左邊的子數組到數組 L
    for(int i = 0; i<n1; i++){
        L[i] = A[p+i];
    }
    
    // 保存右邊的子數組到數組 R
    for(int j =0; j<n2; j++){
        R[j] = A[q+j+1];
    }

    // 記錄數組 L 和 R 被取出元素的數量
    // 防止數組越界
    int L_sum = 0;
    int R_sum = 0;

    // 將 L 和 R 排序保存到主數組中
    for(int k=p; k<=r; k++){

        // 若是左邊數組 (L) 的當前元素 < 右邊數組 (R) 的當前元素
        // 而且左邊的數組尚未越過邊界
        // 則將 (L) 的當前元素追加到子數組中
        if(L[L_sum] < R[R_sum] && L_sum < n1){
            A[k] = L[L_sum];
            L_sum++;
        }
        // 若是右邊數組 (R) 尚未越界
        // 則將 (R) 的當前元素追加到子數組中
        else if(R_sum < n2){
            A[k] = R[R_sum];
            R_sum++;
        }
        // 不然將數組 (L) 追加到子數組中
        else{
            A[k] = L[L_sum];
            L_sum++;
        }
    }
}

僞碼

MERGE(A, p, q, r)
    n1 = q - p + 1
    n2 = r - q
    let L[1..n1 + 1] and R[1..n2 + 1] be new arrays
    for i = 1 to n1
        L[i] = A[p + i - 1]
    for j = 1 to n2
        L[j] = A[q + j]

    // 添加哨兵, 防止數組越界
    L[n1 + 1] = MAX
    L[n2 + 1] = MAX

    i = 1
    j = 1
    
    for k = p to r
        if L[i] <= R[j]
            A[k] = L[i]
            i = i + 1
        else A[k] = R[j]
            j = j + 1

分解大數組爲小數組並遞歸的求解他們

通常環境下除了合併兩個排好序的數組以外更多的是排序一個無序的數組,這裏聊一下怎麼利用上面的那個算法來處理排序一個無序數組(語言表達能力很差,直接看圖吧, 方便起見, 這裏使用 23 個數據爲例)數組

圖解

歸併排序遞歸求解子問題合併步驟.png

C++ 代碼

// 分解一個大的數組
// A: 數組
// p: 子數組的起始
// r: 子數組的結尾
void merge_sort(int * A,int p, int r){
   
    // 當子數組爲 1 的時候中止遞歸
    if(p < r){
        // 獲得當前子數組的中點
        int q = floor((p+r)/2);

        // 遞歸的去分解前半段子數組
        merge_sort(A, p, q);

        // 遞歸的去分解後半段子數組
        merge_sort(A, q+1, r);
        
        // 將子數組合並
        merge(A, p, q, r);
    }
}

僞碼

MERGE-SORT(A, p, q)
    if p < r
        q = ⌊(p + r) / 2⌋
        MERGE-SORT(A, p, q)
        MERGE-SORT(A, q + 1, r)
        MERGE(A, p, q, r)

完整歸併排序 C++ 代碼

/*************************************************************************
> File Name: sf2.cpp
> Author: 
> Mail: 
> Created Time: 2018年06月05日 星期二 00時01分49秒
************************************************************************/
#include<iostream>
#include<math.h>
using namespace std;


// 合併兩個排好序的數組
void merge(int * A,int p, int q, int r);

// 分解一個大的數組
void merge_sort(int * A,int p, int r);

int main(){

    // 測試數據
    int A[9] = {10,9,8,6,5,3,1,2,4};
    
    // 數組的起始 
    int p = 0;
    // 數組的結尾
    int r = 8;

    // 打印測試數據
    cout << "測試數據: ";
    for(int i = 0; i<=r; i++){
        cout << A[i] << " ";
    }
    cout << endl;

    // 排序入口
    merge_sort(A, p, r);

    // 打印運行結果
    cout << "運行結果: ";
    for(int i = 0; i<=r; i++){
        cout << A[i] << " ";
    }
    return 0;
}

// 合併兩個排好序的子數組
// A: 處理的數組
// p: 子數組的起始
// q: 子數組的中點
// r: 子數組的結尾
void merge(int * A,int p, int q, int r){
    
    // 獲取左邊子數組的長度
    int n1 = q - p + 1;

    // 獲取右邊子數組的長度
    int n2 = r - q;
    
    // 建立一個新的數組保存左邊的子數組
    int * L = new int[n1];
    
    // 建立一個新的數組保存右邊的子數組
    int * R = new int[n2];
    
    // 保存左邊的子數組到數組 L
    for(int i = 0; i<n1; i++){
        L[i] = A[p+i];
    }
    
    // 保存右邊的子數組到數組 R
    for(int j =0; j<n2; j++){
        R[j] = A[q+j+1];
    }

    // 記錄數組 L 和 R 被取出元素的數量
    // 防止數組越界
    int L_sum = 0;
    int R_sum = 0;

    // 將 L 和 R 排序保存到主數組中
    for(int k=p; k<=r; k++){

        // 若是左邊數組 (L) 的當前元素 < 右邊數組 (R) 的當前元素
        // 而且左邊的數組尚未越過邊界
        // 則將 (L) 的當前元素追加到子數組中
        if(L[L_sum] < R[R_sum] && L_sum < n1){
            A[k] = L[L_sum];
            L_sum++;
        }
        // 若是右邊數組 (R) 尚未越界
        // 則將 (R) 的當前元素追加到子數組中
        else if(R_sum < n2){
            A[k] = R[R_sum];
            R_sum++;
        }
        // 不然將數組 (L) 追加到子數組中
        else{
            A[k] = L[L_sum];
            L_sum++;
        }
    }
}

// 分解一個大的數組
// A: 數組
// p: 子數組的起始
// r: 子數組的結尾
void merge_sort(int * A,int p, int r){
   
    // 當子數組爲 1 的時候中止遞歸
    if(p < r){
        // 獲得當前子數組的中點
        int q = floor((p+r)/2);

        // 遞歸的去分解前半段子數組
        merge_sort(A, p, q);

        // 遞歸的去分解後半段子數組
        merge_sort(A, q+1, r);
        
        // 將子數組合並
        merge(A, p, q, r);
    }
}

簡單分析一下歸併排序的時間複雜度

方便起見, 這裏使用 2N 個數據爲例, 首先咱們定義一個變量 N 表明 常量 C 表明分解步驟與處理每一個數組元素須要的時間的和(這裏可能不是很是準確,可是不妨礙咱們求解歸併排序算法的最差運行時間, 只是多算了一些分解數組的時間)測試

下圖圖示了歸併排序的歸併樹, 每一層的代價爲 CN 一共有 log2N + 1, 全部的代價和爲 T(N) = CN log2N + CN, 使用大 O 記號去掉常量和低階項獲得該算法時間複雜度O(N log2N)
image.pngspa

相關文章
相關標籤/搜索