c++實現的歸併排序及其優化
普通歸併排序
原理
“分治”思想:對於待排序數據,分成前後兩部分,對每一部分分別進行排序,排完後再進行合併。其中,對於每一部分的排序也是採取這種分兩段排序再合併的思路。
性質
- 時間複雜度
——最好、最壞和平均:O(nlogn) - 空間複雜度
——O(n):非原地排序!! - 穩定性
——穩定
代碼
/***********************************************************
* 未優化歸併
************************************************************/
/**
* @brief 未優化歸併排序使用的合併函數[從小到大](TODO 不完善)
* @param a:待排序數組; start,mid:第一段排好序的待合併元素範圍,相應[mid+1, end]爲第二段
* @return void
* @note
*/
template<typename T>
void merge0(T *a, int start, int mid, int end) {
// 一般可以考慮開闢一個新的滿足總長的數組,然後排序在新的數組上,最後複製回原數組
// 這裏開闢兩個數組存放兩段原數據,直接在原數組上存最終排序,並藉助哨兵使代碼簡潔
int size_left = mid - start + 1; // 左半段數量
int size_right = end - mid; // 右半段數量
T* left = new T[size_left + 1]; // 加1是爲了多加一個哨兵位
T* right = new T[size_right + 1];
int i = 0, j = 0, k = 0; // 循環計數變量
// 複製
for (i = 0; i < size_left; ++i) {
left[i] = a[start+i];
}
for (i = 0; i < size_right; ++i) {
right[i] = a[mid+1+i];
}
// 加哨兵
left[size_left] = MAX;
right[size_right] = MAX;
// 按序生成新的數組
for (k = start, i = 0, j = 0; k <= end; ++k) {
if (left[i] <= right[j]) {
// “<=” 保證穩定性
a[k] = left[i];
++i;
}
else {
a[k] = right[j];
++j;
}
}
// 釋放動態內存
delete [] left;
delete [] right;
}
/**
* @brief 未優化歸併排序使用的遞歸函數[從小到大](TODO 不完善)
* @param a:待排序數組; start,end:待合併元素範圍
* @return void
* @note
* -可改進點: 1.兩段排序後先看是否需要排序2.對於小數組使用插入排序;
*/
template<typename T>
void merge_sort0_r(T *a, int start, int end) {
// 凡遞歸,先考慮遞歸終止條件
if (start >= end) {
return ;
}
int mid = start+((end-start)>>1); // 中間元素下標
// 兩段數據分開進行排序
merge_sort0_r(a, start, mid);
merge_sort0_r(a, mid+1, end);
// 合併
merge0(a, start, mid, end);
}
/**
* @brief 未優化歸併排序(遞歸版)[從小到大](TODO 不完善)
* @param a:待排序數組; len:待排序元素
* @return void 直接修改原
* @note
* -時間複雜度:最好 - O(nlog);最壞 - O(nlogn);平均 - O(nlogn) 【與待排序數據的有序度無關】
* -空間複雜度:O(n); !!!不是原地排序算法!!!
* -穩定性:穩定【取決於merge】
* -改進點: 1:對於arr[mid] <= arr[mid+1]的情況,不進行merge 2:對於小數組, 使用插入排序;
*/
template<typename T>
void merge_sort0(T *a, int len) {
// 首先檢查數據的合法性(TODO 不完善).
if (a == NULL || len <= 1) {
return;
}
// 遞歸進行歸併排序
merge_sort0_r(a, 0, len-1);
}
優化1:merge前增加判斷
原理
如果a[mid] <= a[mid+1],說明已經有序,不用再merge
代碼
template<typename T>
void merge_sort1_r(T *a, int start, int end) {
// 凡遞歸,先考慮遞歸終止條件
if (start >= end) {
return ;
}
int mid = start+((end-start)>>1); // 中間元素下標
// 兩段數據分開進行排序
merge_sort1_r(a, start, mid);
merge_sort1_r(a, mid+1, end);
// 若無序則合併
if (a[mid] > a[mid+1]) {
merge0(a, start, mid, end);
}
}
優化2:小數據使用插入排序
原理
需要排序的數組足夠長時,歸併排序肯定比插入排序更快,但當數組長度比較小的時候,常量因子起主導作用,由於插入排序的常量因子比較小,插入排序比歸併排序更快。
對歸併排序的優化2:當遞歸排序的子問題變得足夠小時,不繼續遞歸調用歸併排序,而是直接調用插入排序。
代碼
template<typename T>
void merge_sort2_r(T *a, int start, int end) {
// 凡遞歸,先考慮遞歸終止條件
if (start >= end) {
return ;
}
// 若數據量小則直接使用插入排序
if (end - start < MIN) {
insert_sort(a, start, end);
}
else {
int mid = start+((end-start)>>1); // 中間元素下標
// 兩段數據分開進行排序
merge_sort2_r(a, start, mid);
merge_sort2_r(a, mid+1, end);
// 若無序則合併
if (a[mid] > a[mid+1]) {
merge0(a, start, mid, end);
}
}
}
測試
以20000個整數進行測試,記錄時間,同時assert是否與algorithm中sort結果相同。測試代碼略,可見完整代碼中。
測試結果時間(同時,assert未報錯)如下圖所示:
可見,優化效率提升比較明顯,而且對於20000個數據,歸併排序優於冒泡排序、插入排序、選擇排序
(詳情見
【從頭學數據結構和算法】冒泡排序及其優化(c++實現)
【從頭學數據結構和算法】插入排序及其優化(c++實現)
【從頭學數據結構和算法】選擇排序及其優化(c++實現)
完整代碼
排序代碼及優化及測試 完整代碼在:
https://github.com/zhangdanzhu/basic_data-structure_algorithm/blob/master/sort/cpp/merge_sort.cpp