前言
上一篇文章提到了几个时间复杂度为O(N^2)的排序算法,它们实现简单,在特定场合效果也不错,但是当数据量大的时候单纯用那几个算法效果就不是很理想了。这时我们时间复杂度为O(NlogN)的算法就有了用武之地,虽然实现相对而言比较困难,但是它的效率在数据量大的情况下是O(N^2)排序算法的数百甚至上千倍,因此在实际生活中是很重要的。
归并排序
原理
归并排序(Merge Sort),采用分治思想,将排序的过程分成一个个子过程,当所有子过程完成时,排序也最终完成了。而归并排序是将数组不断地二分,进而形成一个个子过程。
子过程,即归并的过程如下图演示,需要额外开辟一片空间进行复制一份原数组,然后两边逐一进行比较后进行归并。
至于它的时间复杂度,我们可以看到它的树的高度应该是
这时候我们使用时间复杂度为O(N)的算法进行排序即可完成排序。
代码实现
/** 将arr[L,mid]和arr[mid+1,R]两部分进行归并 */
template<typename T>
void __merge(T arr[], int L, int mid, int R) {
//开辟临时空间
T tem[R - L + 1];
//存储原数值
for(int i = L; i <= R; i++) {
tem[i - L] = arr[i];
}
//初始化索引位置,i为左侧索引,j为右侧
int i = L, j = mid + 1;
//k为应该插入arr的位置
for(int k = L; k <= R; k++) {
//前面两个先判断索引合法性,
if(i > mid) {
//左侧排完了
arr[k] = tem[j - L];
j++;
} else if(j > R) {
//右侧排完了
arr[k] = tem[i - L];
i++;
} else if(tem[i - L] < tem[j - L]){
arr[k] = tem[i - L];
i++;
} else {
arr[k] = tem[j - L];
j++;
}
}
}
/** 递归使用归并排序,对arr[L,R]范围进行排序 L,R为闭区间,R表示最后一个而非最后一个的后面一个 */
template<typename T>
void __mergeSort(T arr[], int L, int R) {
//跳出条件
// if(L >= R)
// return;
//优化代码2,数组较小的时候用插入代替归并,15的取值有待商榷
if( R - L <= 15){
insertionSort(arr, L, R);
return;
}
//注意L+R可能会溢出
int mid = (L + R)/2;
//递归二分
__mergeSort(arr, L, mid);
__mergeSort(arr, mid + 1, R);
//优化代码1
if(arr[mid] > arr[mid + 1])
//归并左右两部分
__merge(arr, L, mid, R);
}
template<typename T>
void mergeSort(T arr[], int n) {
__mergeSort(arr, 0, n-1);
}
优化
上面给出了归并排序的实现,同时优化已经包含在内。
优化1
//优化代码1
if(arr[mid] > arr[mid + 1])
//归并左右两部分
__merge(arr, L, mid, R);
优化代码1在归并处,如果左半边的最右(左半边最大的数)小于右半边的最左(右半边最小的数),那么是不是已经是有序的呢?因此不需要再进行不必要的归并,减少了很多冗余的工作。加了一行代码就完成了针对近乎有序数组的优化。
优化2
//跳出条件
if(L >= R)
return;
这是原来的跳出条件,也是根据定义,当二分到只有一个数的时候应该跳出循环。但是我们可以在这里使用插入排序进行一点优化,这基于两点原因:
- 在数据量较小的情况下,近乎有序的概率较高
- 虽然插入排序的时间复杂度为O(N^2)级别的,但是前面的系数较小,当数据量小的时候比归并排序更快
//优化代码2,数组较小的时候用插入代替归并
if( R - L <= 15){
insertionSort(arr, L, R);
return;
}
这里的数值我本来想做一下实验的,不过受限于编译器,我使用的是codeblocks,时间只显示了四位小数,因此数值很小的时候都显示为0s……因此就使用了15这个数了,我是抱着学习原理的态度来学的,这里也没花太多时间,有兴趣大家可以实验一下,到底哪个数最优。
自底而上的归并排序
上面我们是从上到下,不断地递归分层,然后归并。我们同样可以从下而上,先两个两个归并,然后四个四个,不断增加步长完成归并。演示如下图所示:
代码修改
/** 自底而上的归并排序 没有使用arr的索引,可用于链表的排序 */
template <typename T>
void mergeSortBU(T arr[], int n) {
for( int sz = 1; sz <= n; sz += sz) {
//i+sz<n保证存在第二部分,min解决第二部分数量不足问题
for(int i = 0; i + sz < n; i += sz * 2) {
//对arr[i...i+sz-1]和arr[i+sz...i+2*sz-1]进行归并
__merge( arr, i, i + sz -1, min(i + sz * 2 -1, n - 1) );
}
}
}
归并仍然使用__merge方法,步长sz从1开始不断*2,最终长度大于等于整个数组长度时停止。
自底而上的归并排序结论
这里我们并没有针对近乎有序的情况进行优化,所以速度稍微慢了一点。不过自底而上的归并排序有一个优势,它在实现的过程中没有使用数组下标,因此在迁移至链表的排序时优势会比较大。
图片引用百度图片
代码实现参照liuyubobobo慕课网教程