1. 图解两个有序数组归并
在看归并排序时,我们首先要能够归并两个有序数组,换句话说就是合并两个有序数组为一个有序数组。
例如归并以下两个数组
a[5] = {3,5,7,8,10}
b[7] = {1,2,4,5,8,11,12,}
主要思想:
- 定义一个新数组c,可以容纳a和b两个数组中的所有元素;
- 初始化三个下标(都指向第一个元素),i给a数组,j给b数组,k是新数组c的;
- a[i]和b[j]进行比较:若a[i]<b[j],将a[i]填入c[k],i++,k++;若a[i]>b[j],将b[j]填入c[k],j++,k++;
- 循环第三步,直至其中一个数组中的数据全部填入数组c中,再将另外一个还有剩余的数组中的元素放入新数组c中。
图解过程如下:
代码如下:
int * merge(int* a,int *b,int n,int m){
//有序数组a[n]
//有序数组b[m]
int arr[n+m];//定义一个新的可以容纳a[n]和b[m]所有元素的数组
int i = 0;//i为目前a中数组正在比较的元素的下标
int j = 0;//j为目前b中数组正在比较的元素的下标
int k = 0;//目前c中待填入数的下标
while(i<=n && j<=m){
arr[k++] = (a[i] < b[j]) ? a[i++] : b[j++];
//比较a和b中目前i和j指向的数字
//将较小值填入arr中
//若a[i]较小,i++;否则j++
}
//上一个while循环结束后,只会有一个数组会有剩余数,故而以下两个while只会进入一个
while(i<=n){
arr[k++] = a[i++];
}//当数组a中还有剩余数
while(j<=m){
arr[k++] = b[j++];
}//当数组b中还有剩余数
return arr;
}
2. 图解归并排序
而我们的归并排序就是先使用递归将数组中元素进行划分,直至划分得到单个元素作为一个数组,此时就可以将其看作一个有序数组(只有一个元素自然是有序的)进行归并。最终将所有元素归并得到一个有序数组,所以这种排序方法称作为归并排序。
归并排序主要涉及两个部分:一个是递归问题(如果不了解递归,可以看一看图文详解递归:二分法求数组中的最大值),另一个就是有序数组进行归并,也就是我们第一部分的内容。
例如:将数组a[10] = {5,7,8,9,4,1,3,2,6,10}进行归并排序
需注意上图中数组前的序号是指该有序子数组形成的先后顺序,希望借此大家能更好的理解。
在此我们再使用文字描述一下这个过程:
将子数组分到其中只有一个元素,此时我们就可以认为这个子数组是有序的,然后在得到左右两个这样只有一个元素的子数组后进入归并merge
,得到一个更大的有序数组如圈1时的数组{5,7},其他子数组和这个过程相同,直至将所有其全部归并我们就会得到一个有序数组。
3. 归并排序代码
以上我们就明白了归并算法的基本思路,但还有一些算法的细节需要处理。在上述第一节中我们合并两个有序数组的时候是需要重新定义一个大小为两个数组之和的数组,但是在归并排序中我们不仅要定义这个数组还需要将其重新导入原数组中。这里需要注意两者下标的问题。
我们送进merge的数据段分别为[L,mid]和[mid+1,R],那么新建的数组arr中共有R-L+1个,排好序的数组下标的范围为[0,R-L],我们现在需要将这R-L+1个数平移至数组a下标为[L,R]的段中。
所以我们初始化k=L
,但此时a[L]
中应导入的数为arr[0]
也就是a[k-L]
,具体代码如下:
for(k=L;k<=R;k++){
a[k] = arr[k-L];
}
除此以外,我们并没有使用我们常见的(L+R)/2
计算中点,而是使用L+(R-L)/2
计算中点,这种计算方法好处是防止当数组很大时(L+R)
会溢出,使用后者进行计算就没有这方面的问题。
具体归并排序的代码如下:
#include <stdio.h>
void merge_sort(int* a,int L,int mid,int R);
void process(int* a,int L,int R);
void process(int* a,int L,int R){
if(L == R)
return;
int mid = L+((R-L)/2);//计算中点
process(a,L,mid);
process(a,mid+1,R);
merge_sort(a,L,mid,R);
}
void merge_sort(int* a,int L,int mid,int R){
int arr[R-L+1];
int i = L;
int j = mid+1;
int k = 0;
while(i<=mid && j<=R){
arr[k++] = (a[i] < a[j]) ? a[i++] : a[j++];
}
while(i<=mid){
arr[k++] = a[i++];
}
while(j<=R){
arr[k++] = a[j++];
}
for(k=L;k<=R;k++){
a[k] = arr[k-L];
}
}
int main(){
int n;
printf("数组长度:");
scanf("%d",&n);
int a[n];
printf("请输入一组数:");
for(int i=0;i<n;i++){
scanf("%d",&a[i]);
}
process(a,0,n-1);
for(int i=0;i<n;i++){
printf("%d ",a[i]);
}
printf("\n");
}
3. 归并排序时间复杂度
使用master
公式求解归并问题的时间复杂度(如果不了解master
公式可以看看这一篇使用master公式计算递归问题的时间复杂度)
计算过程如下:
我们使用master
公式计算一下它的时间复杂度为O(NlogN)。相比较选择排序和冒泡排序的O(N^2),我们的归并排序明显时间复杂度比前两者好。前两者明显浪费了大量的比较行为,而递归的归并排序就没有浪费大量的比较行为,它可以看作是两个有序部分合并在了一起,但是它额外空间复杂度为O(logN)。