1. 程式人生 > >資料結構-二路歸併及歸併排序

資料結構-二路歸併及歸併排序

一、介紹:

歸併排序(Merge sort)是建立在歸併操作上的一種有效的排序演算法。該演算法是採用分治法(Divide and Conquer)的一個非常典型的應用。將已有序的子序列合併,得到完全有序的序列;即先使每個子序列有序,再使子序列段間有序。

該演算法的核心思想是二路歸併。

二、二路歸併介紹

<1>在歸併的過程中步驟如下:
①設定兩個指標(不一定非是指標,只需要記住對應開始下標即可),最初位置分別為兩個已經排序序列的起始位置;
②比較兩個指標所指向的元素,選擇相對小的元素放入到合併空間,並移動指標到下一位置;
③重複步驟3直到某一指標達到序列尾;
④將另一序列剩下的所有元素直接複製到合併序列尾;
<2>例如:將a1,a2兩個有序數列合併為一個序列。
void Merge(int* a1, int a1size, int* a2, int a2size, int* res)
{
    //合併兩個有序序列a1 a2 結果儲存在res陣列中
    int a1_index, a2_index, res_index;
    a1_index = a2_index = res_index = 0;
    while (a1_index < a1size && a2_index < a2size)
    {
        if
(a1[a1_index] < a2[a2_index]) res[res_index++] = a1[a1_index++]; else res[res_index++] = a2[a2_index++]; } //將a1或a2剩餘部分插入到res while (a1_index < a1size) res[res_index++] = a1[a1_index++]; while (a2_index < a2size) res[res_index++] = a2[a2_index++]; } int
main() { int a1[] = { 2,3,5 }; int a2[] = { 2,9 }; int a1size = sizeof(a1) / sizeof(a1[0]); int a2size = sizeof(a2) / sizeof(a2[0]); int a3size = a1size + a2size; int* a3 = new int[a3size]; Merge(a1, a1size, a2, a2size, a3); for (int i = 0; i < a3size; i++) cout << a3[i] << " " << endl; system("pause"); return 0; }
輸出結果為:2 2 3 5 9
<3>圖解二路歸併:

這裡寫圖片描述
這裡寫圖片描述
這裡寫圖片描述

三、歸併排序主體思想:

1. Divide: 把長度為n的輸入序列分成兩個長度為n/2的子序列。
2. Conquer: 對這兩個子序列分別採用歸併排序。
3. Combine: 將兩個排序好的子序列合併成一個最終的排序序列。
簡而言之就是將整個陣列劃分到不能再劃分為止,劃分的過程可以通過遞迴來實現而,再依次進行歸併操作,在歸併的過程中,藉助另一個數組來暫時存放該區間排序後的結果,再拷貝回原陣列。來張圖形象的理解下:

這裡寫圖片描述

四、程式碼實現及測試:

#include <iostream>

using namespace std;

//將子序列歸併為一個完整序列
void Merge(int* a, int first, int mid, int last, int* res)
{
    //將a[first mid] a[mid+1 last]排序合併
    int first_start = first, first_end = mid;
    int second_start = mid + 1, second_end = last;
    int res_index = 0;
    while (first_start <= mid && second_start <= last)
    {
        if (a[first_start] < a[second_start])
            res[res_index++] = a[first_start++];
        else
            res[res_index++] = a[second_start++];
    }

    while (first_start <= mid)
        res[res_index++] = a[first_start++];

    while (second_start <= last)
        res[res_index++] = a[second_start++];

    //拷貝回原來的空間
    for (int i = 0; i < res_index; i++)
        a[first + i] = res[i];
}

//遞迴不斷劃分區間
void merge_sort(int* a, int first, int last, int* res)
{
    if (first >= last)
        return;

    int mid = (first + last) / 2;
    merge_sort(a, first, mid, res);     //左邊有序    
    merge_sort(a, mid + 1, last, res);  //右邊有序    
    Merge(a, first, mid, last, res);    //將左右兩個有序數列進行排序歸併
}


bool MergeSort(int a[], int n)
{
    int *res = new int[n];
    if (res == NULL)
        return false;
    merge_sort(a, 0, n - 1, res);
    for (int i = 0; i < n; i++)
        cout << a[i] << " ";

    delete[] res;
    return true;
}


int main()
{
    int a[] = {3,2,5,9,2};
    MergeSort(a, sizeof(a) / sizeof(a[0]));

    system("pause");
    return 0;
}
輸入結果為:2 2 3 5 9

五:圖示整個歸併排序過程:

這裡寫圖片描述

六、優化思路

由於使用遞迴的方式將陣列劃分到不能再劃分為止,再依次進行排序後歸併,棧幀開銷非常大,可以採用小區間優化的方式,在劃分的過程中判斷子序列的元素個數,若小於10(可根據情況而定),可利用插入排序來直接完成此段空間的排序過程。這樣一來就可以減少遞迴次數,尤其是當資料量變大時就會很明顯(插入排序講解參考插入排序講解

七、非遞迴實現

非遞迴實現中,就不需要先劃分為一個再歸併,而是直接可以歸併兩個元素,然後四個,八個…,假如陣列為6,2,8,1,5,4,非遞迴過程圖示如下:

這裡寫圖片描述

程式碼實現:
void Merge(int* a, int first, int mid, int last, int* res)
{
    //將a[first mid] a[mid+1 last]排序合併
    int first_start = first, first_end = mid;
    int second_start = mid + 1, second_end = last;
    int res_index = 0;
    while (first_start <= mid && second_start <= last)
    {
        if (a[first_start] < a[second_start])
            res[res_index++] = a[first_start++];
        else
            res[res_index++] = a[second_start++];
    }

    while (first_start <= mid)
        res[res_index++] = a[first_start++];

    while (second_start <= last)
        res[res_index++] = a[second_start++];

    //拷貝回原來的空間
    for (int i = 0; i < res_index; i++)
        a[first + i] = res[i];
}

//len為陣列元素個數
void MergeSortNR(int* a,int len)
{
    int size = 1; //size為一半部分的元素個數
    int left, right, mid;
    left = right = mid = 0;
    int* tmp = new int[len];

    while (size <= len - 1)
    {
        left = 0;
        while (left + size <= len -1)
        {
            //mid等於left+子序列一半的個數-1;
            mid = left + size - 1;
            right = mid + size;

            //若right超出陣列範圍,right=最後一個元素下標
            if (right > len - 1)
                right = len - 1;

            Merge(a, left, mid, right, tmp);
            left = right + 1;
        }
        size *= 2;
    }
}