归并排序详解[C++递归与非递归实现,复杂度和稳定性分析]

算法原理:

概述:

        ACwing的y总对归并排序有一个口诀,我觉得很便于记忆和理解,叫作:

“双路归并,物归原主”

        归并排序的核心步骤就在于“归并”,我们定义“归并”这个操作,指的就是把两个已经是有序的数组合并起来,并且保证他们合并起来以后仍然有序

        利用这个操作,只要我们能把待排序数组分成有序的两半,再利用上面定义的这个“归并”把它俩合起来,那这整个数组就有序了。

归并(Merge):

        那么“归并”操作说是这样说,具体做起来如何实现呢?这样实现:

//假设待排序数组为R,
//我们现在要把R中[lower,mid]这段和[mid+1,upper]这段给“归并”起来

void Merge(int R[],int lower, int mid,int upper)
{
    int* temp=new int[upper-lower+1];//我们需要借用的临时数组
    int i=lower,j=mid+1,k=0; //i指针用于在[lower,mid]这个区间内索引,j用于在[mid+1,upper]内
                             //k用于往temp里放数据时索引下标位置
    while(i<=mid&&j<=upper)
    {
//同时扫描要合并的两段,把小的元素优先放前面
    if(R[i]<R[j]) temp[k++]=R[i++];
    else     temp[k++]=R[j++];
    }
//有可能一段已经放完了,另一段有剩余元素,
    while(i<=mid) temp[k++]=R[i++];
    while(j<=mid) temp[k++]=R[j++];
    
//至此,我们已经完成了“双路归并”,成功把本来已有序的[lower,mid]和[mid+1,upper]这两段给合并起来,
//并且合并之后仍是有序的,这个合并出来的新数组就存放在temp里

//现在,只需“物归原主”,即把temp找原样搬回R中
    for(int t=0,i=lower;i<=upper;i++,t++)
    {
        R[i]=temp[t];
    }
//“双路归并,物归原主”结束
    delete* temp;
}    
    

        如果看完代码觉得难以理解“归并”究竟是怎样操作的话,我们可以看一个直观的例子(与上面的代码对比结合着看哦):

        我们现在有两个有序的区间,分别为R[low——mid]和R[mid+1——high]。一开始,初始化i=lower,j=mid+1,k=0,X就是temp临时数组,

        比较R[i]和R[j],谁小谁就放到下面的临时数组中,显然R[i]小,无论是R[i]还是R[j],只要放下去了,指针自动后移指向下一个元素,也就是如果R[i]被放下去,i++;如果R[j]被放下去,那么j++。

         

        一直重复这个过程,直到R[low——mid]和R[mid+1——high]这两个区间有一个被遍历完了。

 

        显然可见,这样的操作,每次往X中放元素,放的都是“两个区间中还剩下的元素中的最小的那个 ”,这样就保证了X是有序的。

         现在,右边那个区间被遍历完了,最后一个放入X的元素是39,依据这种规则,那么左边那个区间中剩下的元素,肯定都大于39,那么就挨个放下来(当然如果是左边区间先完了右边区间有剩,那就右边挨个放下来),对应的正是代码中这两行:

        

        至此,正如上面代码中注释所说,“双路归并”已经完成了,我们已经实现了把“两个有序的区间合并起来,且仍保持有序”,只不过结果现在存放在临时数组X里,而我们要排序的是目标数组R,所以接下来自然应该“物归原主”,将X复制回R,然后删除临时创建的临时数组就可以了。

        

        “归并”操作就完成了,那么接上面所说,只要我们能把待排序数组分成有序的两半,再利用上面定义的这个“归并”把它俩合起来,那这整个数组就有序了。

        问题是如何保证左右两半有序?当然需要先把两半各自再分成有序的两半,再归并。那如何保证半个数组的两半是有序的,这样不久无穷尽的分下去了么?什么时候能合并呢?没错,这就构成了自顶向下的递归,而递归问题的出口,我们知道,往往就在递归的最底层

        答案当然是,当分成的子数组只有一个元素时,那么这个子数组是不是有序的?当然是。

        我给你一个数组a,只有一个元素1,那么a是一个有序数组,

        再给你一个数组b,只有一个元素2,那么b是一个有序数组,

        现在让你,“将两有序数组a,b合并,并保证合并后仍是有序数组”,能不能做到?当然能,比较1和2谁大谁小,1小放前面,2大放后面,合成了一个新的数组c ,

int c={1,2};

        我们这不就把两个长度为1的有序数组,合并成一个长度为2的有序数组了?那么在此基础上,递归回溯,我们是不是能把两个长度为2的有序数组,合并成一个长度为4的有序数组?能不能把两个长度为4的有序数组,合并成一个长度为8的有序数组?

        这样一直向上回溯,像我们刚才说的,我们最终能不能把整个待排序数组,变成有序的呢?答案是,当然能。

        

实现:

递归(自顶向下):

void MergeSort(int R[], int m, int n)
{
    if (m < n)
    {
        int k = (m + n) / 2;
        MergeSort(R, m, k);
        MergeSort(R, k + 1, n);
        Merge(R, m, k, n);
    }
}

        用一张图来直观理解:

        递归不断向下深入的过程,就是不断折半、不断缩短区间的过程,直至区间长度为1以后开始向上回溯,回溯的过程中逐步由长度为1的区间归并为长度为2,由长度为2归并为长度为4,这样逐步将整个长度为n的我们待排序的区间给归并排序了。

非递归(自底向上):

void Mpass(int R[],int n,int L){
    //合并相邻的两个长度为L的子数组
    for(int i=1; i+2*L-1≤n; i+=2*L)
        Merge(R, i, i+L–1, i+2*L–1);
    //处理余留的长度小于2*L的子数组
    if(i+L–1 < n)
        Merge(R, i, i+L–1, n); //L<剩余部分长度<2L
}

void MergeSort(int R[], int n){
    for(int L=1; L<n; L*=2) 
        Mpass (R, n, L); 
}

        Mpass每执行一次,是把待排序区间R划分成一个个长度为L的子区间,并将两两相邻的子区间归并,那么子区间的长度,当然应该从1开始取。MergeSort中L从1到n的循环,恰恰对应了刚才递归实现的自底向上的回溯。

时间复杂度分析:

        

        显而易见,每次Merge,要遍历被Merge的两个子区间,所以复杂度O(n),而一共要进行几次Merge?自然取决于这个高度k,也即:

        

        所以最好、最坏、平均情况下,时间复杂度均为O(nlogn)

稳定性分析:

        归并排序是稳定的,(两个值相等的元素,排序后相对次序保持不变),但我太菜无法给出证明,所以就放一个例子吧:

        

  • 13
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
好的,以下是代码实现: ```c #include <stdio.h> #include <stdlib.h> void merge(int arr[], int l, int m, int r) { int i, j, k; int n1 = m - l + 1; int n2 = r - m; int L[n1], R[n2]; for (i = 0; i < n1; i++) L[i] = arr[l + i]; for (j = 0; j < n2; j++) R[j] = arr[m + 1 + j]; i = 0; j = 0; k = l; while (i < n1 && j < n2) { if (L[i] <= R[j]) { arr[k] = L[i]; i++; } else { arr[k] = R[j]; j++; } k++; } while (i < n1) { arr[k] = L[i]; i++; k++; } while (j < n2) { arr[k] = R[j]; j++; k++; } } void mergeSort(int arr[], int l, int r) { if (l < r) { int m = l + (r - l) / 2; mergeSort(arr, l, m); mergeSort(arr, m + 1, r); merge(arr, l, m, r); printf("排序后的结果:"); for (int i = l; i <= r; i++) { printf("%d ", arr[i]); } printf("\n"); } } int main() { int n, i; printf("请输入数组的长度:"); scanf("%d", &n); int arr[n]; printf("请输入数组元素:"); for (i = 0; i < n; i++) scanf("%d", &arr[i]); mergeSort(arr, 0, n - 1); printf("最终排序结果:"); for (i = 0; i < n; i++) printf("%d ", arr[i]); printf("\n"); return 0; } ``` 实现的过程中,我们先定义了两个函数,分别是 `merge()` 和 `mergeSort()`。其中 `merge()` 函数用于将两个已排好序的数组合并成一个有序的数组;`mergeSort()` 函数用于将数组按减序排序。 在 `mergeSort()` 函数中,我们首先对数组进行二分,归地将左右两半分别排序,再将排好序的两个数组合并起来。每完成一次归并操作,我们就输出当前的归并结果。最后,我们输出最终排序结果。 希望能对你有所帮助!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值