归并排序算法

本文深入解析了归并排序算法的工作原理及其实现细节。通过分治策略,归并排序能够高效地处理大规模数据集,达到O(NlogN)的时间复杂度。文章详细介绍了归并排序的分解、解决和合并步骤,并提供了完整的伪代码及C语言实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

归并排序

归并排序以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)例程中,我们需要借助一个队列来存储归并结果,否则原表会发生损坏。我们来分析一下在归并例程中不断申请队列的后果:

  1. 当我们访问到叶时开始回升,在叶处无需操作所以没有申请内存的操作
  2. 每当我们访问到节点时,我们申请在每一层我们都需要申请总长度为n的内存空间
  3. 在depth-1层,我们需要执行n/2次malloc操作,在depth-2层,我们需要执行n/4次malloc操作,依此类推,当我们访问到根时,仍需执行一次malloc操作,总的malloc操作次数为n-1次!
    malloc操作次数
  4. 而为了防止内存冗余,我们又将执行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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值