資料結構與演算法C++之歸併排序
阿新 • • 發佈:2018-11-14
上兩篇部落格使用的選擇排序和插入排序的演算法複雜度都是O(n2),這在陣列元素比較多的時候和一些演算法複雜度為O(nlogn)的快速排序演算法相比,差距還是很明顯的,如下圖
當有10萬個元素的時候,快速排序演算法比普通的選擇排序演算法要快了6000倍,
本篇部落格介紹的是快速排序演算法中的歸併排序
歸併排序是利用歸併的思想實現的排序方法,該演算法採用經典的分治策略(分治法將問題分成一些小的問題然後遞迴求解,而治的階段則將分的階段得到的各答案"修補"在一起,即分而治之)。
可以看到這種結構很像一棵完全二叉樹,本文的歸併排序採用遞迴去實現,遞迴深度為logn。
分階段就是將陣列不斷進行對半切分,直到最後每部分只剩下一個元素為止,此時只有一個元素我們就認為該部分是有序的。
治階段,我們需要將兩個已經有序的子序列合併成一個有序序列,比如上圖中的最後一次合併,要將[4,5,7,8]和[1,2,3,6]兩個已經有序的子序列,合併為最終序列[1,2,3,4,5,6,7,8],來看下實現步驟。
合併階段需要在空間裡另外開闢一個和陣列相同大小的空間來存放排好序的陣列,為上圖的temp
定義指向temp的索引為k,指向左邊序列的索引為i,指向右邊序列的索引為j,左邊序列最後一個元素的索引為mid
採用遞迴方法實現歸併排序:
#include <iostream>
#ifndef _SORTINGHELP_H_
#define _SORTINGHELP_H_
#include "SortingHelp.h"
#endif // _SORTINGHELP_H_
using namespace std;
//將arr[l...mid]和arr[mid+1...r]兩部分進行歸併
template<typename T>
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] = 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 __mergeSorting(T arr[], int l, int r){
if (l >= r)
return;
int mid = (l + r)/2;
__mergeSorting(arr, l, mid);
__mergeSorting(arr, mid+1, r);
if (arr[mid] < arr[mid+1])//如果左邊的序列已經小於右邊的序列,就不用合併了
__merge(arr, l, mid, r);
}
template<typename T>
void MergeSorting(T arr[], int n){
__mergeSorting(arr, 0, n-1);
}
int main()
{
int n = 50000;
int *arr = generateRandomArray(n, 0, n);
int *arr2 = copyIntArray(arr, n);
testSorting("InsertionSortingImproved", InsertionSortingImproved, arr2, n);
testSorting("MergeSorting", MergeSorting, arr2, n);
delete[] arr;//最後刪除陣列開闢的空間
delete[] arr2;
return 0;
}
SortingHelp.h定義為
#include <iostream>
#include <ctime> //time()函式
#include <cstdlib> //rand()函式
#include <cassert> //assert()函式
using namespace std;
int* generateRandomArray(int n, int rangeL, int rangeR){//生成隨機陣列
assert(rangeL < rangeR);
int *arr = new int[n];
srand(time(NULL));
for (int i = 0; i < n; i++){
arr[i] = rand() % (rangeR - rangeL + 1) + rangeL;
}
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;
}
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 testSorting(string sortName, void(*sorting)(T[], int), T arr[], int n){
//第二個引數是傳入排序函式的指標
clock_t startClock = clock();
sorting(arr, n);
clock_t endClock = clock();
assert(isSorted(arr, n));
cout<<sortName<<" : "<<double(endClock-startClock)/CLOCKS_PER_SEC<<" s"<<endl;
return;
}
int* copyIntArray(int arr[], int n){
int* arr2 = new int[n];
copy(arr, arr+n, arr2);
return arr2;
}
template<typename T> //定義模板型別,使對各種資料型別都適用,如double,float,string
void SelectionSorting(T a[], int n){//選擇排序演算法
for (int i = 0; i < n; i++){
int minIndex = i;
for (int j = i + 1; j < n; j++){
if (a[j] < a[minIndex])
minIndex = j;
}
swap(a[i], a[minIndex]);
}
}
template<typename T>
void InsertionSortingImproved(T arr[], int n){
for (int i = 0; i < n - 1; i++){
T temp = arr[i+1];
int j;
for (j = i + 1; j > 0; j--){
if (arr[j-1] > temp){
arr[j] = arr[j-1];
}
else{
break;
}
}
arr[j] = temp;
}
return;
}
執行時間為:
可以看出歸併排序比普通的插入排序確實快了很多倍
對於近乎有序的陣列來說,執行下面的測試程式
int main()
{
int n = 50000;
//int *arr = generateRandomArray(n, 0, n);
int *arr = generateNearlyOrderedArray(n, 10);//生成只有200個無序元素的陣列
int *arr2 = copyIntArray(arr, n);
testSorting("InsertionSortingImproved", InsertionSortingImproved, arr, n);
testSorting("MergeSorting", MergeSorting, arr2, n);
delete[] arr;//最後刪除陣列開闢的空間
delete[] arr2;
return 0;
}
執行時間為
可以看出對於近乎有序的陣列來說,插入排序的效果還是很好的,要優於歸併排序