归并排序
- "归并排序"是数列排序的算法之一。
- 其思路引点来自于著名的“分治”思想和“递归思想”。
“分治,字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。在计算机科学中,分治法就是运用分治思想的一种很重要的算法。”
而递归的思想,做为一种算法在程序设计语言中广泛应用。 一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。递归的能力在于用有限的语句来定义对象的无限集合。一般来说,递归需要有边界条件、递归前进段和递归返回段。当边界条件不满足时,递归前进;当边界条件满足时,递归返回。这种特性与“分治”不谋而合,故大部分的分治算法都是通过递归实现的。
一、算法思路
假设有如下数列:
首先,我们将数列分成两半。
直至数列的最小单位。
接下来,将结合我们分割的每个组。这个过程称为 “合并”。
合并时,按照数字的升序移动(如果你是升序排序的话),使得合并后的数字在组内按升序排列。
当合并包含多个数字的组时,我们只比较开头的数字,移动较小的数字。
在图中,比较左组开头的“4”和右组开头的“3”。
4>3,所以移动“3”。
同样,我们记住,只比较余列的开头数字。
4<7,所以我们移动“4”。
6<7,所以移动“6”。
最后只剩下“7”了,直接移动上去。
因为这个过程是完全相同的,所以我们可以使用递归实现。递归地重复合并操作,直到所有的数字都在一个组中。
等整个过程完成后,就实现了归并排序。
二、动画演示
三、道理都懂了,可是怎么就实现了排序?
其实,刚接触归并排序的人,会困于两个点:分治和合并。
分治,其实思想很简单,不断的划分,但很多人卡在了实现的过程中,用什么实现?如何实现?
上文,我们讲到,递归与分治的思想部分吻合,因此用递归实现分治的划分是一个不错的选择。
/*
* Method mergeSort has three parameters,the first of them is initial array,then,second is the initiative index of current array,finally third one is the last index of current array.
* 归并排序方法有三个参数,第一个是初始的数组,第二个是该数组的起始索引,第三是该数组的尾巴索引。
*/
void mergeSort(int *ms,int startIndex,int endIndex){
//如果数列划分至最小单位(一个数)则停止分割
if(endIndex-startIndex>0){
//将数列分为左右部分进行分治
mergeSort(ms,startIndex,(startIndex+endIndex)/2);//左分治
mergeSort(ms,((startIndex+endIndex)/2)+1,endIndex);//右分治
merge(ms,startIndex,endIndex);//归并
}
}
因为这个划分的过程就是不断的划分划分…通过递归,不断的调用自身,从而实现参数的暂存,数据的缓存,利用一个函数,将整个大问题,划分至单位问题。
第二个点就是合并。其实分治与合并相比,是很简单的。合并的难点在于,我们到底怎么合并?用什么合并?合并后会发生什么?
这里作者建议使用数组的特性,因为我们利用函数传递的是数组元素头的地址,而不是整个数组,或一个新的数组,所以我们可以通过指针实现将所有的操作在初始待排序的数组上实现。
void merge(int *ms,int startIndex,int endIndex){
......
}
那么只剩下解决最后一个问题:如何合并。
这里试想一个问题:将两个数组,排序后,放置到一个新数组中,该怎么做?
这个问题咋一看似乎很简单?实际上存在着也许技巧。
这里作者提供两个方法供大家思考。
- 利用C++的结束符
我们都知道C++中,字符串,数组等都会默认的在生成时在结尾添加一个结束符,方便我们输出,那么我们就可以利用这个结束符。
首先,用上文算法思路里提到的,我们只比较两个数组的最左端(其实是指针所指向的最左),将较小的存入新数组中(其实我们会使用初始待排序的那个数组来存,节省空间),不断重复这个过程,当不论是左数组到头了,还是右数组到头了,我们利用if判断是否到了结束符即可,再将还有剩余的数组剩下的全部存入新数组即可。但,我不推荐这个方法,太直接,太笨了。 - 利用数组的length
这个方法我很推荐,在于,我们利用了现有的资源。同上,我们我们只比较两个数组的最左端(其实是指针所指向的最左),将较小的存入新数组中(其实我们会使用初始待排序的那个数组来存,节省空间),不断重复这个过程,直到,我们的if判断,判定其中的一个数组到达了它的长度,那么我们就将另一个有剩余的数组剩下的全部存入新数组即可。
方法2看似很容易,但实现起来,需要很细心的去利用index,下面只贴法2的代码,请读者细细体会,跟着代码走一走。
void merge(int *ms,int startIndex,int endIndex){
//进入归并步骤时,数组将由两个数组合并,升序排序划分,左边的称之为左数组,同理,右边的称之为右数组。
int left_mid = (startIndex+endIndex)/2;//待定左数组的右边界
int mid_right = ((startIndex+endIndex)/2)+1;//待定右数组的左边界
int left_length = (left_mid-startIndex)+1;//待定左数组长度
int right_length = (endIndex-mid_right)+1;//待定右数组长度
int left_array[left_length];//初始化左数组
int right_array[right_length];//初始化右数组
for(int i=left_mid;i>=startIndex;i--){//将左数组挂起
left_array[i-startIndex] = ms[i];
}
for(int i=endIndex;i>=mid_right;i--){//将右数组挂起
right_array[i-mid_right] = ms[i];
}
int l_index=0,r_index=0;//将两个指针指向左、右数组的头元素
//双数组循环排序,复杂度O(n),排序后的结果直接赋回原数组ms上
for(int i=startIndex;i<=endIndex;i++){
if(l_index!=left_length && r_index!=right_length){
if(left_array[l_index]<right_array[r_index]){
ms[i] = left_array[l_index++];
}
else{
ms[i] = right_array[r_index++];
}
}
else if(l_index==left_length){
ms[i] = right_array[r_index++];
}
else{
ms[i] = left_array[l_index++];
}
}
}
四、代码清单及其测试结果
#include <iostream>
#include <ctime>
template <class T>
int getSizeOfArray(T& bs){
return sizeof(bs)/ sizeof(bs[0]);
}
/*
* Method merge has three parameters,the first of them is initial array,then,second is the initiative index of current array,finally third one is the last index of current array.
* 归并方法有三个参数,第一个是初始的数组,第二个是该数组的起始索引,第三是该数组的尾巴索引。
*/
void merge(int *ms,int startIndex,int endIndex){
//进入归并步骤时,数组将由两个数组合并,升序排序划分,左边的称之为左数组,同理,右边的称之为右数组。
int left_mid = (startIndex+endIndex)/2;//待定左数组的右边界
int mid_right = ((startIndex+endIndex)/2)+1;//待定右数组的左边界
int left_length = (left_mid-startIndex)+1;//待定左数组长度
int right_length = (endIndex-mid_right)+1;//待定右数组长度
int left_array[left_length];//初始化左数组
int right_array[right_length];//初始化右数组
for(int i=left_mid;i>=startIndex;i--){//将左数组挂起
left_array[i-startIndex] = ms[i];
}
for(int i=endIndex;i>=mid_right;i--){//将右数组挂起
right_array[i-mid_right] = ms[i];
}
int l_index=0,r_index=0;//将两个指针指向左、右数组的头元素
//双数组循环排序,复杂度O(n),排序后的结果直接赋回原数组ms上
for(int i=startIndex;i<=endIndex;i++){
if(l_index!=left_length && r_index!=right_length){
if(left_array[l_index]<right_array[r_index]){
ms[i] = left_array[l_index++];
}
else{
ms[i] = right_array[r_index++];
}
}
else if(l_index==left_length){
ms[i] = right_array[r_index++];
}
else{
ms[i] = left_array[l_index++];
}
}
}
/*
* Method mergeSort has three parameters,the first of them is initial array,then,second is the initiative index of current array,finally third one is the last index of current array.
* 归并排序方法有三个参数,第一个是初始的数组,第二个是该数组的起始索引,第三是该数组的尾巴索引。
*/
void mergeSort(int *ms,int startIndex,int endIndex){
//如果数列划分至最小单位(一个数)则停止分割
if(endIndex-startIndex>0){
//将数列分为左右部分进行分治
mergeSort(ms,startIndex,(startIndex+endIndex)/2);//左分治
mergeSort(ms,((startIndex+endIndex)/2)+1,endIndex);//右分治
merge(ms,startIndex,endIndex);//归并
}
}
int main() {
using namespace std;
int ms[] = {2,3,5,1,0,8,6,9,7};
int size = getSizeOfArray(ms);
cout<< "原数列:";
for(int i = 0;i<size;i++)
{
cout<< ms[i] << " ";
}
cout<< "\n" << "归并排序后:";
mergeSort(ms,0,size-1);
for(int i = 0;i<size;i++)
{
cout<< ms[i] << " ";
}
return 0;
}
五、算法分析
随机数范围:r属于[0,100]的整数
样本数(单位:个) | 10 | 100 | 1,000 | 10,000 | 100,000 | 1,000,000 |
---|---|---|---|---|---|---|
运行时间(单位:秒) | 3*10-6 | 1.5*10-5 | 0.000201 | 0.002218 | 0.01838 | 0.2 |
归并排序比较占用内存,但却是一种效率高且稳定的算法。
改进归并排序在归并时先判断前段序列的最大值与后段序列最小值的关系再确定是否进行复制比较。如果前段序列的最大值小于等于后段序列最小值,则说明序列可以直接形成一段有序序列不需要再归并,反之则需要。所以在序列本身有序的情况下时间复杂度可以降至O(n)
TimSort可以说是归并排序的终极优化版本,主要思想就是检测序列中的天然有序子段(若检测到严格降序子段则翻转序列为升序子段)。在最好情况下无论升序还是降序都可以使时间复杂度降至为O(n),具有很强的自适应性。