归并思想
归并排序是采用分治策略(二分)方法将一个数组不断的分隔成若干子序列,这些子序列都是有序的,然后对这些子序列逐步合并,合并的结果也有序,这样不断的操作,最终将整个数组进行排序。
从这里也可以看出,如果需要合并某两个序列,则要求两个序列必须内部是已经排好序的才可以,而这就是归并排序的分解过程,
归并排序的图解
归并图解:
假设有一个名为arr的数组,其元素为{1,3,5,2,4,6},则其归并的过程如下:
一步归并的实现
//merge_array方法,用来归并一个数组中的两个部分,这两个部分已经是内部排好序的
template<class T>
void merge_array(T arr[],const int left,const int middle,const int right){//其中,arr表示要排序的数组,left表示要排序的整个部分的左边界,而middle表示左边有序部分的右边界,right表示右边有序部分的右边界
T temp[right-left+1]; //创建一个数组,其长度等于原数组
int i = left; //等于左边排好序的部分的左边界
int j = middle+1; //等于右边排好序的左边界
int k = 0; //这个是用来指向temp数组的
int m = 0; //用于最后将结果复制到原数组arr中
//开始归并
while(i<=middle && j<=right){ //遍历数组排好序的两个部分,其中i用来遍历左边的部分,j用于遍历右边的部分
if(arr[i]<=arr[j]){ //如果左边的部分的元素小于等于右边部分的,这里只能是小于等于,不能写小于,因为归并过程是一个稳定过程,而归并排序也是一个稳定排序
temp[k]=arr[i]; //将左边部分的元素写入temp
k++; //给k加1,让其右移,为下一次向temp中写入数据做准备
i++; //i所指向的元素已经遍历过,然后往右继续
}
else if(arr[i]>arr[j]){ //如果如果左边的部分的元素大于右边部分的
temp[k]=arr[j]; //将右边部分的元素写入temp
k++; //给k加1,让其右移,为下一次向temp中写入数据做准备
j++; //右移j,遍历下一个元素
}
}
//此时,左右两个部分中可能还会有一些剩下的元素,这个在上面归并的图解过程中已经体现了出来(其在最后一步)。这里就是将某个部分中剩余的元素填入temp数组
//写入左边部分剩余的元素
while(i<=middle){
temp[k] = arr[i];
k++;
i++;
}
//写入右边部分剩余的元素
while(j<=right){
temp[k] = arr[j];
k++;
j++;
}
//最后,将排好序的数组复制到原数组即可,因为用于保存排序数组的数组temp会在函数执行完后自动销毁
while(m<k){ //这里不是等于是因为在上面的过程中,在最后一次向temp中写入元素后,还给k加了1,所以这里要写小于,否则会造成越界
arr[left+m] = temp[m]; //表示这是从左边界起第m个排好序的数
m++;
}
}
测试:
int main(){
int arr[]={1,3,5,2,4,6};
merge_array(arr,0,2,5); //表示要排序的部分的左边界为0,右边界为5,左边排好序的部分的右边界为2
for(int i=0;i<6;i++){
cout<<arr[i]<<endl;
}
} //结果为:1,2,3,4,5,6
归并排序的递归实现
下面,就开始利用上面的一步递归的方法来实现归并排序
template<class M>
void merge_sort(M arr[],int left,int right){ //left和right表示要排序的左边界和右边界
if(left>=right) return; //如果left大于等于right,停止递归
//求出中点
int middle = left+(right-left)/2; //这里可以防止left+right的值过大
//对左半部分进行递归
merge_sort(arr,left,middle);
//对右半部分进行递归
merge_sort(arr,middle+1,right);
//对传入进来的数据调用归并函数
merge_array(arr,left,middle,right);
}
测试程序:
int main(){
int arr[]={2,1,5,8,7,6};
merge_sort(arr,0,5); //调用归并排序方法,左边界为0,右边界为5
for(int i=0;i<6;i++){
cout<<arr[i]<<endl;
}
} //结果为:1,2,5,6,7,8
测试程序调用归并排序执行的过程
上面是merge_sort(arr,0,2)这一函数递归的过程,merge_sort(arr,3,5)的过程和其类似,这里不再细说。最后得到两个排好序的序列0到2和3到5,然后对这两个序列再进行归并,就可以得到最后的排序结果。
归并排序的迭代实现
首先,对于单次归并的过程并不改变,即merge_array()方法不改变。只改变merge_sort()方法中的一些内容。
此方法参考自《数据结构算法与应用——C++语言描述》
//merge_pass:用来确定归并的序列的两端
template<class A>
void merge_pass(A a[],const int step,const int len){
//归并长度为step的相邻段
int i=0;
while(i<(len-2*step)){ //i表示归并第几个相邻段,i表示的是步长为step时需要合并的每个子序列的首位的下标,而(len-2*step)表示有多少个步长相同的可以归并的序列,比如当len为6,而step为2时,表示只有两个步长相同的可以进行归并的序列,也就是说只能够归并一次(因为每次归并需要两个序列)。这点也可以参看后面的解析(步长为2时各参数的关系)
merge_array(a,i,i+step-1,i+2*step-1);
i = i+2*step;
}
//两两合并后剩下的元素
if(i+step < len){ //表示按照相同step归并之后,剩下两个步长不同的子序列,对其进行合并
merge_array(a,i,i+step-1,len-1); //表示按照
}
//此时,可能还剩下单独一个元素或者序列不能归并,将其继续保留在原数组中即可
}
//归并算法的非递归实现
template<class T>
void merge_sort1(T a[],const int len){
int step = 1; //步长,即是合并序列的长度,刚开始为1
while(step<len){ //当步长小于整个数组的长度的时候
merge_pass(a,step,len); //确定要归并的子序列的左边界和右边界
step = step*2; //步长的变化规律为每次归并的长度为上一次的两倍,如第一次归并长度为1,第二次为2,第三次为4...
}
}
迭代实现归并排序的图解
迭代的过程比起递归的过程,少了逐步拆分数组的过程,而是直接将数组的每个元素当成一个子序列,逐步进行归并;而递归是将数组逐步拆分成单个元素的子序列,再逐步进行归并,由此也可以看到,迭代实现的归并排序的效率要高于递归实现的归并排序。
以int arr[]={2,1,5,8,7,6};
为例,下面是其用迭代法归并的图解:
具体的一次的归并过程参考上面归并的图解。
归并排序迭代实现方式的测试程序
int main(){
int arr[]={2,1,5,8,7,6};
merge_sort1(arr,6); //调用归并排序方法,左边界为0,右边界为5
for(int i=0;i<6;i++){
cout<<arr[i]<<endl;
}
} //结果为:1,2,5,6,7,8
迭代实现时各个参数的含义(以实例中的数组为例)
1、 刚开始各项参数的值:
此时的图解:
2、 下来各项参数的值
3、 下来各项参数的值
4、当步长为2时刚开始各项值的关系
5、当步长为4时,各个参数之间的关系