• 【C语言】算法学习·归并排序


    目录

    归并排序

    算法描述

    自顶向下和自底向上

    实例 

    代码

    递归实现

    迭代实现 

    进阶·原地归并


    归并排序

    算法描述

    归并排序是 分治思想 的应用,即将原待排数组 递归或迭代地 分为左右两半,直到数组长度为1,然后对左右数组进行合并(merge),在合并中完成排序。详细过程需结合代码理解,如下动图展示了{4,6,2,1,7,9,5,8,3}的归并排序过程(自顶向下非原地)。合并过程采用 非原地 合并方法,即依次比较两部分已排序数组,将比较结果依次写入 新空间 中。后续会介绍一种称作 原地(in-place) 归并排序的改进,使得空间复杂度达到 常数级 (自底向上时,O(1))。

    如下树状图中的橙色线表示递归的轨迹(自顶向下递归归并排序)。

    稳定性:稳定。

    合并时的此判断中的等号if(left[l_next] <= right[r_next]),保证了出现相等元素时,居左的元素总会被放在左侧,稳定性不受影响。

    自顶向下和自底向上

    可以通过 自顶向下(top-down) 或 自底向上(bottom-up) 的方式实现归并排序。

    自顶向下(top-down):从输入数组出发,不断二分该数组,直到数组长度为1,再执行合并。适合用 递归 实现。

    自底向上(bottom-up):从输入数组的单个元素出发,一一合并,二二合并,四四合并直到数组有序。适合用 迭代 实现。

    实例 

    a[]={1,3,5,7,2,4,6,8};


    对于这个数组,利用归并的思想排序,就是把它分成两个部分,从中间截开,分成两组数:1,3,5,7和2,4,6,8;我们可以发现两组数都是从小到大排序的,我们可以定义两个变量一个指向前一串数的第一个数字,另一个变量指向第二组数的第一个变量,分别比较这两个数,将小的那个放进一个新数组,然后变量往后移,逐个比较,最终就有了一个新数组,这个新数组就是排序好的数组。
    但是如果分成两组数之后,两边的数字并不是有序的该怎么办?这时候说明把数组分开一次不够,就要继续再分,如果还不是有序的?再分,直到把它们分为一个一个的数,然后再用归并的思想把它们重新排回原来的数组,整个数组就变得有序了。

    代码

    递归实现

    1. void merge_sort(int a[],int left,int right){
    2.     if(left
    3.         int mid = (left + right) / 2;//从中间截开
    4.         merge_sort(a,left, mid);//把左边沿中间截开
    5.         merge_sort(a, mid + 1, right);//把右边沿中间截开
    6.         merge(a, left, right, mid);//合并
    7.     }
    8. }
    9. //接下来这个函数是合并的过程。
    10. void merge(int a[],int left,int right,int mid) {
    11.     int s[100];//一个新数组用来存储排序好的数组
    12.     int i = left, j = mid + 1;//两个变量分别指向左边和右边两组数的第一个数
    13.     int sor = left;
    14.     while (i <= mid && j <= right) {
    15.         if (a[i] < a[j]) {//归并的过程
    16.             s[sor++] = a[i++];
    17.         }
    18.         else {
    19.             s[sor++] = a[j++];
    20.         }
    21.     }
    22.     while (i <= mid) s[sor++] = a[i++];//当一组数已经全部排进去之后,再将另外一组数的全部数字都排进去
    23.     while (j <= right)  s[sor++] = a[j++];
    24.     sor = left;
    25.     while (sor <= right) {//把排好序的新数组全部放回原数组里
    26.         a[sor] = s[sor];
    27.         sor++;
    28.     }
    29. }

    迭代实现 

    1. void mergesort(int num[],int len){
    2. //对数组num归并排序迭代实现,len为数组长度,从小到大排序,O(nlog2^n),稳定
    3. /*核心思想,i表示步长,也就是左右两组各几个元素比较
    4. ,从第一轮左右每组1个开始,每轮步长增大一倍
    5. ,比较后从小到大存入temp,再对剩余元素进行处理
    6. ,最后将排好序的temp返回num数组
    7. */
    8. //分别为步长、temp下标、左边起始下标、左边终点下标、右边起始下标、右边终止下标
    9. int i,next,left_min,left_max,right_min,right_max;
    10. //新建一个temp数组,长度等于初始数组长度
    11. int *temp = (int*)malloc(len * sizeof(int));
    12. //每轮比较左右两个步长i长度的区间,每轮后i*=2
    13. for(i=1; i2){
    14. //从数组0号开始,下一组的起始位置等于上一组的终止位置,如果下一组左边步长都不够就不比了
    15. for(left_min=0; left_min < len-i; left_min = right_max){
    16. //右边起始位置=左边终止位置=左边起始加步长i
    17. right_min = left_max = left_min + i;
    18. //右边终止位置=右边起始位置加步长i
    19. right_max = right_min + i;
    20. next = 0;//temp的下标
    21. if(right_max > len){//如果右边越界
    22. right_max = len;//右边终止位置最大值只能为len
    23. }
    24. while(left_min < left_max && right_min < right_max){//左右都没到尽头
    25. if(num[left_min] < num[right_min]){//左小右大,左边存入temp
    26. temp[next++] = num[left_min++];
    27. }else{//右小左大,右边存入temp
    28. temp[next++] = num[right_min++];
    29. }
    30. }
    31. /*左边还有一组剩余元素,右边已到终止位置
    32. ,说明左边剩余元素最大,将剩余元素移到右边最后
    33. ,如果是右边有剩余,则不需要移了已经在最后*/
    34. while(left_min < left_max){
    35. num[--right_min] = num[--left_max];
    36. }
    37. while(next > 0){//把排好序的temp部分返回num
    38. num[--right_min] = temp[--next];
    39. }
    40. }
    41. }
    42. }

    进阶·原地归并

    前述归并排序,每一次合并都是将两部分待合并数组的比较结果写入一个与arr等大小的临时数组tmpArr中,写入后再将tmpArr中的合并结果写回到arr中。于是tmpArr的空间开销即为该实现的空间复杂度,为 O(n)。实际上,通过一种 原地旋转交换 的方法(俗称手摇算法/内存反转算法/三重反转算法),则只需要 O(1) 的辅助空间(由于递归空间为O(logn),其总的空间复杂度仍为 O(logn))。以下介绍旋转交换的实现方法。

    以 456123 为例,欲将 456 和 123 交换位置转换为 123456,只需要执行三次旋转即可:

    旋转 456,得到 654
    旋转 123,得到 321
    旋转 654321 得到 123456。
    应用上述「手摇算法」对两个排序序列的「原地归并」过程如下。

    1. 记左数组第一个数下标为i,记右数组第一个数下标为j。
    2. 找到左数组中第一个 大于 右数组第一个数字的数,记其下标为i。
    3. 以index暂存右数组第一个元素的下标index = j。
    4. 找到右数组中第一个 大于等于 arr[i]的数,记其下标为j。此时必有 [i, index - 1]下标范围序列大于 [index, j - 1] 下标范围序列。
    5. 通过三次翻转交换 [i, index-1] 和 [index, j - 1] 序列 (指下标范围),即依次翻转[i, index-1],翻转[index, j - 1],翻转[i, j - 1]。
    6. 重复上述过程直到不满足(i < j && j <= rightEnd)

    ※ 第4步如果找「大于」而不是「大于等于」,对于数字数组排序,结果正确,但将 破坏稳定性。建议动手画一下。

    以{1, 2, 4, 6, 7}与{3, 5, 8, 9} 两个已排序序列的合并为例,观察借助手摇算法实现原地归并的过程。

    在{1, 2, 4, 6, 7}中找到第一个大于3的数4,其下标为2,i = 2。index = j = 5。在{3, 5, 8, 9}中找到第一个大于arr[i] = arr[2] = 4的数5,其下标为6,j = 6。
    如上操作使得[0, i - 1]必是最小序列,[index, j - 1]必小于arr[i]。因此交换[i, index - 1]和[index, j - 1](采用三次旋转完成交换),使得这部分序列在整个数组中有序。
    交换后,继续执行上述过程,直到不满足该条件 :i < j && j <= rightEnd。

     

  • 相关阅读:
    设计模式_spring框架中常用的8种设计模式
    OSCAR数据库上锁问题如何排查
    【Hadoop---02】Hadoop简介
    zabbix集群搭建分布式监控的操作步骤
    微前端四:qiankun在开发中遇到的问题
    OpenAI首届开发者大会多项更新汇总
    java-net-php-python-46jspm制衣厂后整管理系统计算机毕业设计程序
    【Java八股文总结】之JDK常见问题排查
    java 商机管理系统Myeclipse开发mysql数据库web结构jsp编程计算机网页项目
    ECharts综合案例一:近七天跑步数据
  • 原文地址:https://blog.csdn.net/m0_64560763/article/details/126032131