排序算法(四) —— 归并排序

来源:互联网 发布:mac隐藏菜单栏快捷键 编辑:程序博客网 时间:2024/05/07 16:36

原创文章,转载请注明出处。

本文是排序算法系列文章的第四篇,主要讲述 “归并排序” 算法。

一、原理:

       合并排序也即归并排序,是将两个或两个以上的有序表合并成一个新的有序表的操作,是分治法的一个典型应用。

       先使每个子序列有序(段内有序),再使子序列段间有序,最后合并后得到完全有序的序列。归并排序可以使用递归实现,递归的条件是序列起始位置(startIndex)小于序列结束位置(endIndex),即序列元素大于 1。

       先将原序列一分为二,再分别对子序列一和子序列二进行递归排序操作,并不断对相邻两个序列执行归并操作,直至序列完全有序。设子序列一为数组 a,子序列二为数组 b,合并的新序列为数组 c。归并的过程为:比较 a[i] 和 b[j] 的大小,若 a[i] 小于等于 b[j],则将第一个有序表中的元素 a[i] 复制到 c[k] 中,并令 i 和 k 分别加上1;否则将第二个有序表中的元素 b[j] 复制到 c[k] 中,并令 j 和 k 分别加上 1,如此循环下去,直到其中一个有序表的元素被取完,然后将另一个未取完元素的有序表中的剩余元素复制到 c 中,添加到已有元素后面。最后将新序列 c 拷贝并覆盖原序列,完成对原序列的排序操作。

二、代码及注释:

void merge(int *arrA, int *arrB, int startIndex, int middleIndex, int endIndex){int i = startIndex, j = middleIndex + 1, k = startIndex;//i为有序序列一的起始位置,j为有序序列二的起始位置,k为合并序列起始位置while (i < middleIndex + 1 && j < endIndex + 1)//当两个有序序列均存在待合并元素时,即两个序列均未取完时{if (arrA[i] <= arrA[j])//若序列一的元素小于序列二的元素{arrB[k] = arrA[i];//取序列一的元素放入新序列i++;//i+1,下标移至下一个元素,表示前一个元素已取出}else//若序列二的元素小于序列一的元素{arrB[k] = arrA[j];//取序列二的元素放入新序列j++;//j+1,下标移至下一个元素,表示前一个元素已取出}k++;//新序列每增加一个元素则下标+1}while (i < middleIndex + 1)//若序列二取完了而序列一未取完{arrB[k] = arrA[i];//将序列一剩余元素全都按序添加至新序列队尾i++;k++;}while (j < endIndex + 1)//若序列一取完了而序列二未取完{arrB[k] = arrA[j];//将序列二剩余元素全都按序添加至新序列队尾j++;k++;}for (i = startIndex; i <= endIndex; i++){arrA[i] = arrB[i];//将存放新序列的辅助数组中的元素拷贝回原数组,使原数组有序,且为合并后的序列}}void mergeSort(int *arrA, int *arrB, int startIndex, int endIndex)//归并排序需要跟原数组同样大小的数组作为辅助空间{int middleIndex;if (startIndex < endIndex)//当序列元素个数大于1时{middleIndex = (startIndex + endIndex) / 2;//将序列一分为二mergeSort(arrA, arrB, startIndex, middleIndex);//序列一继续递归采用归并排序mergeSort(arrA, arrB, middleIndex + 1, endIndex);//序列二继续递归采用归并排序merge(arrA, arrB, startIndex, middleIndex, endIndex);//将有序序列一二进行合并}}
三、算法性能分析:

       当输入规模为 n = 10000、20000、30000、40000、50000 时,执行程序,可以得到在不同输入规模下,20 组随机样本数据执行归并排序的单组执行时间及平均执行时间为:


       理论上来说,归并排序的时间复杂度为 O(nlogn)。同时,由上图可以看出,当 n = 10000 时,20 组样本排序的平均执行为 1.07594 ms。以输入规模为 10000 的数据运行时间为基准点,则设理论上的平均执行时间为:t = O(nlogn),当 n 扩大两倍时,t’ = O(2nlog2n) =O(2n(logn + log2)) = O(2nlogn + 2nlog2) ≈ O(2nlogn);同理 n 扩大 3 倍、4 倍、5 倍时,t’ ≈ O(3nlogn)、O(4nlogn)、O(5nlogn),相应地,实测耗时为:t’ = 2t、3t、4t、5t,代入t = 1.07594 ms,可以计算出合并排序的理论耗时及实测耗时。

       所以,在相应输入规模下,合并排序的理论执行时间为:


       根据上表,可以作出归并排序理论效率曲线和实测效率曲线如下:


       如上图表,是合并排序的理论效率曲线和实测效率曲线,其中位于上方的数据标签是实测效率曲线的标注,位于下方的数据标签是理论效率曲线的标注。由图表我们可以看出,在问题规模较小时,合并排序的理论耗时和实测耗时时间相近,随着问题规模的增大,实测耗时略高于理论耗时,并且可能随着输入规模的增大二者的差距会逐渐增大。

       在归并排序过程中,不断地将待排序列一分为二,然后进行归并排序操作。上述操作使用递归实现,将序列分为各个子序列直至子序列中仅有一个元素,而后开始两两进行合并,合并成新的序列后退出深层递归返回上一层递归,再与刚才其余序列合并的新序列进行两两合并,如此下去,直至序列完全有序。若待排序记录为 n 个,则需要做 logn 趟归并,每趟归并的时间复杂度为 O(n),因此,总的时间复杂度为 O(nlogn),其曲线应当近乎符合线性关系,从上面的折线图也可以比较直观地看出,不管是理论效率曲线还是实测效率曲线,都基本符合线性关系。

       在输入规模较小时,合并排序的理论耗时和实测耗时时间相近,随着问题规模的增大,实测耗时略高于理论耗时,并且可能随着输入规模的增大二者的差距会逐渐增大,原因可能是在 n 较小时,递归调用耗费时间对总时间影响不大,而随着输入规模地增大,递归调用的次数越来越多,系统对各个递归式的处理和协调耗费了更多的时间,从而对最终结果造成了一定的影响。当然,也可能跟程序运行时电脑的状态有一定关系。