一、基本概念
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年。