归并排序
归并,即“递归合并”。
归并排序的主要思路
Q:我们排序的目的是什么?
A:让无序的序列变得有序。
比如说,下面有一个序列:
排序前 | 排序后 |
---|---|
7654321 | 1234567 |
如果我们将这个序列从中间附近分开,分成两个子序列,想办法让这两个子序列变得有序。然后再把两个有序的子序列合并成一个有序的子序列,那么我们的排序不就完成了吗?
如下图:
这样一个大的任务被分成了两个子任务:对两个子序列进行排序
排序前 | 排序后 |
---|---|
7654 | 4567 |
321 | 123 |
递归
很容易发现,这两个子任务和原来的任务没有本质区别,只是规模变得更小了。可以对这两个子任务再次进行拆分,在代码实现上,这就是递归的操作。
一个基本事实:当序列中只有一个元素时,它是一个有序的序列
有了这个基本事实,不难想出:只要经过足够多次数的分割,一定会将序列分成只有一个元素的序列,这就是递归的出口。
如此,可以给出归并排序主体思想的代码:
这个代码十分简洁:
void MergeSort(int arr[],int start,int end,int temp[])//传入的参数:待排序数组,起始下标,终止下标,辅助数组temp
{
if(start>=end)return;//递归出口
int mid=start+(end-start)/2;//定位到中间附近的下标
MergeSort(arr, start, mid, temp);//对中点及中点以前的元素进行递归
MergeSort(arr, mid+1, end, temp);//对中点以后的元素进行递归
Merge(arr,start,mid,end,temp);//对两个有序的序列进行合并
}
合并
那么,如何进行合并的操作呢?
用两个下标(i,j),分别指向两个有序序列的起始点,再用一个下标(k),指向辅助数组temp的相应位置。
比较i和j对应元素的大小,因为是要排成一个递增的序列,选取较小的赋值给temp[k],再将这个下标和k都加1。
当两个序列都走完后,将temp中的元素复制回待排序的数组。
两个序列中有相等关键词怎么办?
显然,i是严格小于j的,所以i对应的元素在序列中的相对位置是要比j对应的元素靠前的。
k是从小往大走,为了保证排序的稳定性,当关键词相等时,应该选取i对应的元素,再将i加1。
排序的稳定性: 如果关键词相等的两个元素在排序前后相对位置不变,那么称这个排序算法是稳定的。
归并排序是个稳定的排序算法,稳定性由上述操作保证。
下面给出合并操作的代码实现:
void Merge(int arr[],int start ,int mid,int end,int temp[])
{
int i=start;//定位左面的序列
int j=mid+1;//定位右面的序列
int k=start;//定位temp的位置
while(i<=mid||j<=end)//当两个序列中有没走完的序列时,进入循环
if(i<=mid&&j<=end)//如果两个序列都没走完
temp[k++]=arr[i]<=arr[j]?arr[i++]:arr[j++];//选取小的元素赋值,并将下标++
else
temp[k++]=i<=mid?arr[i++]:arr[j++];//只有一个序列没走完,就哪个没走完要哪个
for(int a=start;a<=end;a++)//将元素复制回待排序数组
arr[a]=temp[a];
}
将合并的代码放在排序代码之前,就能执行了。
归并排序是一个时间复杂度为线性对数阶的排序算法,时间复杂度为:O(n*log2n)(以2为底n的对数),要占用至少n个元素的辅助空间。