归并排序(C++) – 速度与空间有机结合
归并排序也是一个O(nlogn)的算法,之所以称这个算法为速度与空间的有机结合,是因为这个算法既能做到O(nlogn)时间赋值度的效率,又能借助外部的空间进行排序,因为只有数据比较的时候需要将数据存入到主机中进行,别的时候数据都可以存储在外部存储中,当然这种情况下就多了很多数据的读取和写入,效率会有很大影响,但能力还是在的;所以要最大程度发挥出O(nlogn)算法的效率,还是要用内部的存储空间,且需要进行优化;算法实现分自顶向下和自底向上两种~~
归并排序之所以能达到O(nlogn),主要用到了数据结构中的二分法,将庞大的数据拆分成两段,不断分不断分直到只有两个数据时进行比较,比较完合并,然后再两个数据和另外两个数据进行比较,比较完合并,然后再四个数据和另外四个数据进行比较,比较完合并,直至所有数据合并,对比于每个数据都要和别的数据比较一次的O(n2)方法,时间效率大大提升,当然是在数据量庞大的时候;
归并排序 – 自顶向下
自顶向下用的就是递归,将大数组递归拆分两段到两个比较,因为递归的特性,思路和代码体现就是从大到小地进行递归,所以是自顶向下;
实现中涉及到三个方法,从第三个方法mergeSortTopDown往上看,注意区分第二个方法是__mergeSortTopDown;
// 合并方法
template <typename T>
void __merge(T arr[], T arrTemp[], int L, int Mid, int RightEnd) {
int i = L;
int j = Mid+1;
// k是排好序存入的临时数组的下标
int k = L;
// 左右两部分数据未遍历完时循环遍历
while(i<=Mid && j<=RightEnd) {
// 左边时L至Mid,右边时Mid+1至RightEnd
// ++放在后面,是先赋值,再使k、i加1
if(arr[i]<arr[j]) arrTemp[k++] = arr[i++];
else arrTemp[k++] = arr[j++];
}
// 上面while循环结束后,左边部分或右边部分可能会有剩余未比较的(因为当其中一部分比较完了就退出循环了),
// 需要将剩余的数据加入到临时数组中
while(i <= Mid) {
arrTemp[k++] = arr[i++];
}
while(j <= RightEnd) {
arrTemp[k++] = arr[j++];
}
// 将临时数组中已有序部分复制到原数组,使之这部分有序
for(int tail=L;tail<=RightEnd;tail++) {
arr[tail] = arrTemp[tail];
}
}
// 实际递归调用的归并排序方法
template <typename T>
// L为拆分出来的左边界,RightEnd为右边界
void __mergeSortTopDown(T arr[], T arrTemp[], int L, int RightEnd) {
// 判断是否只有一个数据,不是时递归进行二分拆分,排序后归并,如果已经只有一个数据(L=RightEnd)则不用操作
if (L < RightEnd) {
int Mid = (L + RightEnd) / 2;
// 二分后左右边部分递归
__mergeSortTopDown(arr,arrTemp,L,Mid);
__mergeSortTopDown(arr,arrTemp,Mid+1,RightEnd);
__merge(arr,arrTemp,L,Mid,RightEnd);
}
}
// 归并排序,递归玩法,这种玩法时自顶向下的
// 这个方法主要是为了统一接口,方便测试和调用
template <typename T>
// mergeSortTopDown提供统一接口,__mergeSort为实际排序方法
void mergeSortTopDown(T arr[], int n) {
int L = 0;
int RightEnd = n-1;
// arrTemp是用于归并时临时存储有序结果的数组,之所以在这里创建因为空间的申请和释放需要资源和时间,
// 归并排序会频繁进行数据归并操作,在主函数中申请空间后直接传递指针,减少了这部分时间
T* arrTemp = new T[n];
__mergeSortTopDown(arr,arrTemp,L,RightEnd);
delete[] arrTemp;
}
自顶向下归并排序的优化实现:
- 数据量小于等于20个时用插入排序:所有算法不管是O(n2)还是O(nlogn)前面都会有一个常数(如O(Anlongn),A是一个常数),只是当n很大时,这个系数被忽略不计了,当这n较小时,这个系数就有影响了,归并排序前面常数较大,所以当用复杂算法排好大部分数据后改用插入排序(特别是n较小时序列基本有序的可能性也大了)进行排序;
- 因为进行归并时,左边和右边的两部分都是已经有序的,所以如果左边部分的右边界数据已经小于右边部分的左边界数据,那就没必要再进行归并操作了,所以可以加if语句进行判断,这样对于基本有序序列能大大提高效率;然而对于随机序列,如果每次if都是在做无用功,那么if语句带来的耗时可能就降低效率了;
- 接口方法和合并方法没有进行改动,复用上面的;
// 实际递归调用的归并排序方法
template <typename T>
// L为拆分出来的左边界,RightEnd为右边界
void __mergeSortTopDown(T arr[], T arrTemp[], int L, int RightEnd) {
// 优化一:当需要排序的数据小于等于20个时用插入排序
if(RightEnd-L<=20) {
insertSortTool(arr,L,RightEnd);
return;
}
// 判断是否只有一个数据,不是时递归进行二分拆分,排序后归并,如果已经只有一个数据(L=RightEnd)则不用操作
//if (L < RightEnd) {
int Mid = (L + RightEnd) / 2;
__mergeSortTopDown(arr,arrTemp,L,Mid);
__mergeSortTopDown(arr,arrTemp,Mid+1,RightEnd);
// 优化二:边界判断
// 因为左边部分和右边部分都已有序,只有当左边的右边界大于右边的左边界时才有必要进行归并
// 只有当处理的数据有可能近乎有序时才加这个,因为if语句也是额外损耗
if(arr[Mid] > arr[Mid+1])
__merge(arr,arrTemp,L,Mid,RightEnd);
//}
}
归并排序 – 自底向上
自底向上用的时回溯,回溯在内存使用上要比递归少,因为递归中每一次递归使用自己的方法都要占据内存;这里回溯用到了for语句的嵌套使用;与自顶向下相反,从1个数据一组开始,也就是一开始两个数据比较,然后乘2,以此类推;自底向上不需要统一接口,直接调用就可以了,另外合并的方法与上面一样,不再重复列出
// 归并排序,自底向上玩法
template <typename T>
void mergeSortBottomUp(T arr[], int n) {
T* arrTemp = new T[n];
// 从1个数据一组开始
for(int i=1;i<n;i*=2)
// 按每个间隔为一组相互比较,然后合并成原来2倍的数据量
for(int j=0;j<n-i;j+=i*2)
__merge(arr,arrTemp,j,j+i-1,min(j+2*i-1,n-1));
delete[] arrTemp;
}
自底向上归并排序的优化实现:优化思路和自顶向下一样,也是数据量小时先用插入排序然后,边界先比较再决定是否要进行归并
// 归并排序,自底向上玩法
// 优化过后的自底向上和自顶向下性能差不多
template <typename T>
void mergeSortBottomUp(T arr[], int n) {
T* arrTemp = new T[n];
// 优化一:插入排序对每20个数据进行排序,让每20个数据都是有序的,给下面的归并排序做准备
for(int i=0;i<n;i+=20) {
insertSortTool(arr,i,min(i+20,n-1));
}
// 从20个数据一组开始
for(int i=20;i<n;i*=2)
for(int j=0;j<n-i;j+=i*2)
// 优化二:边界判断
if(arr[j+i-1]>arr[j+i])
__merge(arr,arrTemp,j,j+i-1,min(j+2*i-1,n-1));
delete[] arrTemp;
}
插入排序与归并排序性能PK赛
比赛规则:
- 对100万个数据进行排序;
- 分随机数序列和基本有序序列两场比赛;
- 基本有序序列的未有序数据数量是200个;
- 所有排序法均已优化;
- 从上到下是插入排序、自顶向下归并排序、自底向上归并排序;
随机数序列:
insertSort:530.557
mergeSortTopDown:0.135
mergeSortBottomUp:0.138
基本有序序列:
insertSort:0.007
mergeSortTopDown:0.005
mergeSortBottomUp:0.004
比赛结果:
- 毫无疑问,在面对随机的100万个数据时,O(logn2)的插入排序和O(nlongn)的归并排序不在一个level上;但是面对基本有序序列时,插入排序可以说扳回了一城;另外归并排序在数据量低时用到了插入排序,所以说插入排序虽然在面对随机的大量数据时无法作为主排序方法,但是打辅助还是杠杠的;
- 优化过后的自顶向下和自底向上耗时差不多,没有太多差别;但是自顶向下因为用的递归,占用内存较大;