归并排序
归并排序以O(N logN)最坏情形时间运行而所使用的比较次数几乎是最优的。它是分治算法的一个很好的实例。
-
首先我们使用分治模式分析排序问题
- 分解:分解待排序的n个元素的表成各具n/2个元素的子表
- 解决:使用归并排序递归地排序两个子表
- 合并:合并两个已排序好的子表以产生已排序的答案
-
分解
-
因为当表长度n为1时无法再分,所以我们将n=1作为递归过 程的基准情形,当待排序的表长度为1时开始回升,这种情况下我们不需要进行操作,因为长度为1的表已经排好序
- 这些子表的表可以表示成平衡二叉树,根为原表,每个节点是当前表,每个左孩子是这个这个表的前半部分,每个右孩子是这个表的后半部分,为了方便,我们在n为奇数的情况下,将中间数归位左孩子
- 这样,当n为1时我们就访问到了这个平衡二叉树的叶
-
解决
-
合并成对的子表,因为两个表都是按照升序排列的,对他们进行归并后产生的表也是升序的
-
合并
-
当访问到叶并进行合并后,开始向上回升,不断地归并当前节点的两个子节点,最后完成整个表的排序,这里对应的是树的遍历中的后续遍历
伪代码如下
procedure mergesort(L=[a[0]..a[n]])
/*L以非降序排列*/
if n>1 then
mid:=⌊n/2⌋
L1:=L[a[0]..a[mid]]
L2:=L[a[mid]..a[n]]
mergeSort(L1)
mergeSort(L2)
merge(L1,L2)
在归并(merge)例程中,我们需要借助一个队列来存储归并结果,否则原表会发生损坏。我们来分析一下在归并例程中不断申请队列的后果:
- 当我们访问到叶时开始回升,在叶处无需操作所以没有申请内存的操作
- 每当我们访问到节点时,我们申请在每一层我们都需要申请总长度为n的内存空间
- 在depth-1层,我们需要执行n/2次malloc操作,在depth-2层,我们需要执行n/4次malloc操作,依此类推,当我们访问到根时,仍需执行一次malloc操作,总的malloc操作次数为n-1次!
- 而为了防止内存冗余,我们又将执行n-1次free操作,这将产生很大的时间开销,这是我们所不愿意看见的
然而我们不难发现,对于任意一次归并操作,即使是到了根节点,我们所需要的队列大小仍未超过N,所欲我们只需要申请一次队列,以[1..n]为有效长度,即可满足我们的需求
mergeSort
//执行递归操作的函数主体
void mergeSort(int*begin,int len,int*tmp){
if(len>1){
int mid=len>>1;
mergeSort(begin, mid, tmp);
mergeSort(begin+mid, len-mid, tmp);
merge(begin, len,tmp);
}
}
归并排序的主题已经完成,接下来讨论merge例程
- 引理:对两个排好序的表进行归并,最多只需要n+m-1次比较 因为我们这两个表是由一个长度为2n的表产生,所以我们最多只需要2n次比较即可完成归并操作,而且我们可以仅将这个两个表的父表作为参数,通过下标进行分割,而不产生额外的操作
- 值得注意的是,因为这两个表都是非降序表,所以若左表的最大元素的数值小于右表的最小值,则表示表已有序,无需操作;若右表的最大值小于左表的最小值,则只需要置换两表的位置即可
merge
void merge(int*begin,int len,int*tmp){
int mid=len>>1;
//若左表的最大元素的数值小于右表的最小值,则表示表已有序,不操作
if(begin[mid-1]<begin[mid])return ;
int i,j;
//若右表的最大值小于左表的最小值,则只需要置换两表的位置即可
if(begin[0]>begin[len-1]){
j=0;
for(i=mid;i<len;i++)
tmp[j++]=begin[i];
for(i=0;i<mid;i++)
tmp[j++]=begin[i];
}else{
int k=0;
i=0,j=mid;
while(1){
//当两个子表任意一个访问完成,可以记直接将另一个表的所有元素倒入队列中
if(i==mid){
while(j!=len)tmp[k++]=begin[j++];
break;
}else if(j==len){
while(i!=mid)tmp[k++]=begin[i++];
break;
}
//不断向队列中插入较小的元素
tmp[k++]=(begin[i]<begin[j])?begin[i++]:begin[j++];
}
}
//将排好序的队列中的元素放回原表
for(i=0;i<len;i++)
begin[i]=tmp[i];
}
当然,对于使用来说,在每次使用之前需要做准备工作,这对使用来说是不友好的,所以我们需要些一个辅助函数来封装起来这些准备工作
MergeSort
//我们排序所使用的接口
bool MergeSort(int*arr,int len){
int*tmp=malloc(sizeof(int)*len);
if(tmp==NULL){
/*因内存空间问题不是算法所关注的问题,
所以这里只提需要处理无法申请内存的情况
而不讨论内存的处理方法
*/
Error("没有足够大的内存来申请数组");
return false;
}
mergeSort(arr, len, tmp);//开始调用排序的函数主体
free(tmp);
return true;
}