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