歸併排序詳細思路與插入排序的對比

一、基本概念

1.歸併概念:將兩個有序數列合併成一個有序數列,我們稱之爲“歸併”。

2. 歸併排序(Merge Sort)概念

建立在歸併操作上的一種排序算法,該算法是採用分治法(Divide and Conquer)的一個非常典型的應用。

歸併排序有多路歸併排序、兩路歸併排序,可用與內排序,也可用於外排序。

3.算法思路及實現

設兩個有序的子序列(相當於輸入序列)放在同一序列中相鄰的位置上:array[L..m],array[m + 1..R],先將它們合併到一個局部的暫存序列 temp (相當於輸出序列)中,待合併完成後將 temp 複製回 array[L..R]中,從而完成排序。

在具體的合併過程中,設置 i,j 和 p 三個指針,其初值分別指向這三個記錄區的起始位置。合併時依次比較 array[i] 和 array[j] 的關鍵字,取關鍵字較小(或較大)的記錄複製到 temp[p] 中,然後將被複制記錄的指針 i 或 j 加 1,以及指向複製位置的指針 p 加 1。重複這一過程直至兩個輸入的子序列有一個已全部複製完畢(不妨稱其爲空),此時將另一非空的子序列中剩餘記錄依次複製到 array 中即可。

4.具體步驟:

  • 分解 -- 將當前區間一分爲二,即求分裂點 mid = (L + R)/2;
  • 求解 -- 遞歸地對兩個子區間a[L...mid] 和 a[mid+1...R]進行歸併排序。遞歸的終結條件是子區間長度爲1。
  • 合併 -- 將已排序的兩個子區間a[L...mid]和 a[mid+1...R]歸併爲一個有序的區間a[L...R]。

二、(1)圖解實現:

歸併排序(Merge Sort)

當我們要排序數組的時候,使用歸併排序法,將數組分成兩半。如圖:

然後把左邊的數組和右邊的數組排序,最後一起歸併。當我們對左邊的數組和右邊數組進行排序的時候,再分別將左邊的數組和右邊的數組分成一半,然後對每一個部分先排序,再歸併。(也就是一次次遞歸進行分組排序,歸併,再分組再排序再歸併過程)如圖:

對於上面的每一個部分呢,我們依然是先將他們分半,再歸併,如圖:

分到一定細度的時候,每一個部分就只有一個元素了,那麼我們此時不用排序,對他們進行一次簡單的歸併就好了。如圖:

直至最後歸併完成。

 

歸併的細節:

兩個已經排序好的數組,如何歸併成一個數組?

我們可以開闢一個臨時數組來輔助我們的歸併。也就是說他比我們插入排序也好,選擇排序也好多使用了存儲的空間,也就是說他需要o(n)的額外空間來完成這個排序。只不過現在計算機中時間的效率要比空間的效率重要的多。

整體來講我們要使用三個索引來在數組內進行追蹤。
 

2. 排序穩定性

所謂排序穩定性,是指如果在排序的序列中,存在兩個相等的兩個元素,排序前和排序後他們的相對位置不發生變化的話,我們就說這個排序算法是穩定的。

排序算法是穩定的算法。

 藍色的箭頭表示最終選擇的位置,而紅色的箭頭表示兩個數組當前要比較的元素,比如當前是2與1比較,1比2小,所以1放到藍色的箭頭中,藍色的箭頭後移,1的箭頭後移。

然後2與4比較,2比4小那麼2到藍色的箭頭中,藍色箭頭後移,2後移,繼續比較.......

 二、(2)代碼實現:

版本一:

#include<iostream>
#include<algorithm>
//#include "SortTestHelper.h"
//#include "SelectionSort.h"
#include<stdio.h>
using namespace std;
//歸併排序
template<typename T>//泛型
//歸併操作

//將arr[l,mid]和[mid+1,r]兩部分進行歸併操作
void __merge(T arr[],int l,int mid,int r){
	T aux[r-l+1];//臨時存放的數組,開闢的空間
	for(int i=l;i<=r;i++)
		aux[i-l] = aux[i]; //有一個l的偏移量,並且完成臨時空間
	//首先我設置兩個索引已經排好序的兩部分子數組
	int i = l,j = mid+=1;
	for(int k=l;k<=r;k++){
		//判斷數組索引越界問題,當i>mid後面還沒有進行完操作、
		if(i>mid){
			arr[k] = aux[j-l];
			j++;
		}
		else if(j>r){
			arr[k] = aux[i-l];
			i++;
		}
		else if(arr[i-l]<arr[j-l]){
			aux[k] = aux[i-l];
			i++;//索引到下一個位置
		}else{
			aux[k] = aux[j-l];
			j++;//索引到下一個位置
		}
	}
}


template<typename T>//泛型
//遞歸使用歸併排序,對arr[l,r]的範圍進行排序
void __mergeSort(T arr[] ,int l,int r){
	if(l>=r){//這是一個不可能的情況,等於時候,也就是說沒有數據需要處理
		return ;
	}
	//定義中間的數
	int mid = (l+r)/2;
	//開始對分開的左右兩個部分分別進行歸併排序
	__mergeSort(arr,l,mid);//對左邊進行歸併排序

	__mergeSort(arr,mid+1,r);//對右邊就行歸併排序
	__merge(arr,l,mid,r);//將兩段進行merge,歸併或者是融合操作
}


template<typename T>//泛型

void mergeSort(T arr[], int n){
	__mergeSort(arr,0,n-1);
}
int main(){

int a[105],n,i;
	scanf("%d",&n);
	for(i=0;i<n;i++)
	scanf("%d",&a[i]);
    
    mergeSort(a,n);
    sort(a,a+n);
	for(i=0;i<n;i++)
	printf("%d ",a[i]);

	return 0;
}

結果:

測試對比插入排序與歸併排序的時間的複雜度代碼:

main.cpp:

#include <iostream>
#include "SortTestHelper.h"
#include "InsertionSort.h"

using namespace std;
// 將arr[l...mid]和arr[mid+1...r]兩部分進行歸併
template<typename  T>
void __merge(T arr[], int l, int mid, int r){

    // 經測試,傳遞aux數組的性能效果並不好
    T aux[r-l+1];
    for( int i = l ; i <= r; i ++ )
        aux[i-l] = arr[i];

    int i = l, j = mid+1;
    for( int k = l ; k <= r; k ++ ){

        if( i > mid )   { arr[k] = aux[j-l]; j ++;}
        else if( j > r ){ arr[k] = aux[i-l]; i ++;}
        else if( aux[i-l] < aux[j-l] ){ arr[k] = aux[i-l]; i ++;}
        else                          { arr[k] = aux[j-l]; j ++;}
    }
}

// 遞歸使用歸併排序,對arr[l...r]的範圍進行排序
template<typename T>
void __mergeSort(T arr[], int l, int r){

    if( l >= r )
        return;

    int mid = (l+r)/2;
    __mergeSort(arr, l, mid);
    __mergeSort(arr, mid+1, r);
    __merge(arr, l, mid, r);
}

template<typename T>
void mergeSort(T arr[], int n){

    __mergeSort( arr , 0 , n-1 );
}


int main() {

    int n = 50000;

    // 測試1 一般性測試
    cout<<"Test for Random Array, size = "<<n<<", random range [0, "<<n<<"]"<<endl;
    int* arr1 = SortTestHelper::generateRandomArray(n,0,n);
    int* arr2 = SortTestHelper::copyIntArray(arr1, n);

    SortTestHelper::testSort("Insertion Sort", insertionSort, arr1, n);
    SortTestHelper::testSort("Merge Sort",     mergeSort,     arr2, n);

    delete[] arr1;
    delete[] arr2;

    cout<<endl;


    // 測試2 測試近乎有序的數組
    int swapTimes = 100;
    cout<<"Test for Random Nearly Ordered Array, size = "<<n<<", swap time = "<<swapTimes<<endl;
    arr1 = SortTestHelper::generateNearlyOrderedArray(n,swapTimes);
    arr2 = SortTestHelper::copyIntArray(arr1, n);

    SortTestHelper::testSort("Insertion Sort", insertionSort, arr1, n);
    SortTestHelper::testSort("Merge Sort",     mergeSort,     arr2, n);

    delete(arr1);
    delete(arr2);

    return 0;
}

SortTestHelper.h:

#ifndef INC_04_INSERTION_SORT_SORTTESTHELPER_H
#define INC_04_INSERTION_SORT_SORTTESTHELPER_H
#include <iostream>
#include <algorithm>
#include <string>
#include <ctime>
#include <cassert>

using namespace std;


namespace SortTestHelper {

    int *generateRandomArray(int n, int range_l, int range_r) {

        int *arr = new int[n];

        srand(time(NULL));
        for (int i = 0; i < n; i++)
            arr[i] = rand() % (range_r - range_l + 1) + range_l;
        return arr;
    }

    int *generateNearlyOrderedArray(int n, int swapTimes){

        int *arr = new int[n];
        for(int i = 0 ; i < n ; i ++ )
            arr[i] = i;

        srand(time(NULL));
        for( int i = 0 ; i < swapTimes ; i ++ ){
            int posx = rand()%n;
            int posy = rand()%n;
            swap( arr[posx] , arr[posy] );
        }

        return arr;
    }

    int *copyIntArray(int a[], int n){

        int *arr = new int[n];
        copy(a, a+n, arr);
        return arr;
    }

    template<typename T>
    void printArray(T arr[], int n) {

        for (int i = 0; i < n; i++)
            cout << arr[i] << " ";
        cout << endl;

        return;
    }

    template<typename T>
    bool isSorted(T arr[], int n) {

        for (int i = 0; i < n - 1; i++)
            if (arr[i] > arr[i + 1])
                return false;

        return true;
    }

    template<typename T>
    void testSort(const string &sortName, void (*sort)(T[], int), T arr[], int n) {

        clock_t startTime = clock();
        sort(arr, n);
        clock_t endTime = clock();
        cout << sortName << " : " << double(endTime - startTime) / CLOCKS_PER_SEC << " s"<<endl;

        assert(isSorted(arr, n));

        return;
    }

};
#endif //INC_04_INSERTION_SORT_SORTTESTHELPER_H

InsertionSort.h:

#include <iostream>
#include "SortTestHelper.h"
#include "InsertionSort.h"

using namespace std;
// 將arr[l...mid]和arr[mid+1...r]兩部分進行歸併
template<typename  T>
void __merge(T arr[], int l, int mid, int r){

    // 經測試,傳遞aux數組的性能效果並不好
    T aux[r-l+1];
    for( int i = l ; i <= r; i ++ )
        aux[i-l] = arr[i];

    int i = l, j = mid+1;
    for( int k = l ; k <= r; k ++ ){

        if( i > mid )   { arr[k] = aux[j-l]; j ++;}
        else if( j > r ){ arr[k] = aux[i-l]; i ++;}
        else if( aux[i-l] < aux[j-l] ){ arr[k] = aux[i-l]; i ++;}
        else                          { arr[k] = aux[j-l]; j ++;}
    }
}

// 遞歸使用歸併排序,對arr[l...r]的範圍進行排序
template<typename T>
void __mergeSort(T arr[], int l, int r){

    if( l >= r )
        return;

    int mid = (l+r)/2;
    __mergeSort(arr, l, mid);
    __mergeSort(arr, mid+1, r);
    __merge(arr, l, mid, r);
}

template<typename T>
void mergeSort(T arr[], int n){

    __mergeSort( arr , 0 , n-1 );
}


int main() {

    int n = 50000;

    // 測試1 一般性測試
    cout<<"Test for Random Array, size = "<<n<<", random range [0, "<<n<<"]"<<endl;
    int* arr1 = SortTestHelper::generateRandomArray(n,0,n);
    int* arr2 = SortTestHelper::copyIntArray(arr1, n);

    SortTestHelper::testSort("Insertion Sort", insertionSort, arr1, n);
    SortTestHelper::testSort("Merge Sort",     mergeSort,     arr2, n);

    delete[] arr1;
    delete[] arr2;

    cout<<endl;


    // 測試2 測試近乎有序的數組
    int swapTimes = 100;
    cout<<"Test for Random Nearly Ordered Array, size = "<<n<<", swap time = "<<swapTimes<<endl;
    arr1 = SortTestHelper::generateNearlyOrderedArray(n,swapTimes);
    arr2 = SortTestHelper::copyIntArray(arr1, n);

    SortTestHelper::testSort("Insertion Sort", insertionSort, arr1, n);
    SortTestHelper::testSort("Merge Sort",     mergeSort,     arr2, n);

    delete(arr1);
    delete(arr2);

    return 0;
}

 測試結果:

三、時間複雜度及穩定性

1. 時間複雜度

長度爲n的序列需要進行logn次二路歸併才能完成排序(歸併排序的形式其實就是一棵二叉樹,需要遍歷的次數就是二叉樹的深度),而每趟歸併的時間複雜度爲O(n),因此歸併排序的時間複雜度爲O(nlogn)

算法複雜度:O(nlogn)

也許有很多同學說,原來也學過很多O(n^2)或者O(n^3)的排序算法,有的可能優化一下能到O(n)的時間複雜度,但是在計算機中都是很快的執行完了,沒有看出來算法優化的步驟,那麼我想說有可能是你當時使用的測試用例太小了,我們可以簡單的做一下比較

當數據量很大的時候 nlogn的優勢將會比n^2越來越大,當n=10^5的時候,nlogn的算法要比n^2的算法快6000倍,那麼6000倍是什麼概念呢,就是如果我們要處理一個數據集,用nlogn的算法要處理一天的話,用n^2的算法將要處理6020天。這就基本相當於是15年。

 

 

 

 

 

 

發佈了195 篇原創文章 · 獲贊 138 · 訪問量 9萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章