归并排序算法

来源:互联网 发布:linux 线程数过多 编辑:程序博客网 时间:2024/06/16 16:10

归并排序

归并排序以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;}
2 0