• 归并排序详解(递归+非递归)


    目录

    归并排序

    归并排序的思想

    递归实现

    非递归实现


    归并排序

    归并排序和之前讲的快速排序、希尔排序、堆排序一样,时间复杂度是O(N*logN)

    它相对难理解一点,接下来我就从递归以及非递归两个方面详细介绍一下这种排序。

    归并排序的思想

    一个数组从中间将其分为左右两个区间,如果左右区间都是有序的,那么就可以进行归并,分别从两个区间取小的数尾插到新开辟的数组中,不断取小的尾插直到排完。

    那么如果两个区间没有序呢?—— 那就再将这个问题划分子问题,左区间进行上述操作,右区间也进行上述操作......一直分治下去,直到左右区间只剩一个数,就划分为最小子问题,无需再往下分了,这个过程也就是递归的过程。

    这就是归并排序的思想,划分子问题,分而治之。

    递归实现

    假设有一个数组a[10] ={9,6,5,3,8,7,1,2,0,4} 要将它排成升序,可以先找出数组的中间位置,将数组从中间分开,划为两个区间,分别将两个区间排成有序的

    那如何将这两个区间排成有序的呢?————再重复上面步骤分别划分左右两个区间,再将其排成有序的,重复上述步骤......直到左右区间都分的只剩一个数,那也就可以认为它是有序的。整个过程就是一个递归调用的过程

    大家觉得这个递归过程像什么呢?  是不是有点像二叉树。这里的归并递归过程有点类似二叉树里面求树的高度,先递归计算左右子树的高度,都是用的后序遍历。

    先将大体框架完成一下:

    1. void _MergeSort(int* a, int begin, int end,int* tmp)
    2. {
    3. if (begin >= end)
    4. return;
    5. int mid = begin + (end - begin) / 2;
    6. _MergeSort(a,begin, mid,tmp);
    7. _MergeSort(a,mid+1,end,tmp);
    8. //归并
    9. }
    10. void MergeSort(int* a, int n)
    11. {
    12. int* tmp = (int*)malloc(sizeof(int) * n);
    13. if (tmp == NULL)
    14. {
    15. perror("malloc fail");
    16. return;
    17. }
    18. _MergeSort(a, tmp, 0, n-1);
    19. free(tmp);
    20. tmp = NULL;
    21. }
    22. int main()
    23. {
    24. int a[10] = { 9,6,5,3,8,7,1,2,0,4 };
    25. int n = sizeof(a) / sizeof(a[0]);
    26. MergeSort(a, n);
    27. for (int i = 0; i < n; ++i)
    28. {
    29. printf("%d ", a[i]);
    30. }
    31. return 0;
    32. }

    归并排序一般用两个子函数,这样方便一些。

     先计算中间位置,得出左右区间,对左右区间递归排序,直到各自只剩一个数时返回。

     上面是局部递归展开图(归并过程没写),可借助展开图来理解递归过程。

    接下来实现归并细节。

    依据上图,先将左右区间的边界值表示出来:左区间 [begin,mid] 右区间 [mid+1,end]

    找到每一组里面小的那个数,尾插到新开辟出的数组里,不断地取小的尾插,尾插一次就memcpy拷贝回原数组。

    1. void _MergeSort(int* a, int begin, int end, int* tmp)
    2. {
    3. if (begin >= end)
    4. return;
    5. int mid = begin + (end - begin) / 2;
    6. _MergeSort(a,begin, mid,tmp);
    7. _MergeSort(a,mid+1,end,tmp);
    8. //归并
    9. int i = begin;
    10. int begin1 = begin, end1 = mid,
    11. begin2 = mid + 1, end2 = end;
    12. while (begin1 <= end1 && begin2 <= end2)
    13. {
    14. if (a[begin1] <= a[begin2])
    15. {
    16. tmp[i++] = a[begin1++];
    17. }
    18. else
    19. {
    20. tmp[i++] = a[begin2++];
    21. }
    22. }
    23. while (begin1 <= end1)
    24. {
    25. tmp[i++] = a[begin1++];
    26. }
    27. while (begin2 <= end2)
    28. {
    29. tmp[i++] = a[begin2++];
    30. }
    31. memcpy(a + begin, tmp + begin, sizeof(int) * (end - begin + 1));
    32. }

     时间复杂度:O(N*logN), 递归深度为logN,每一层递归因为要遍历选小的数,所以是O(N),合起来就是O(N*logN)

    空间复杂度:O(N),开辟了额外的tmp空间,这里不及快排,快排空间复杂度是O(logN)

    非递归实现

    在深度了解递归的情况下,我们也可以用非递归来实现。非递归实现还是有一定难度的,不太好理解,建议将递归展开图多画画,吃透了再实现非递归。

    归并排序的非递归,我们不借助栈或队列等结构实现,直接用迭代手撕。

    其本质和递归异曲同工,只是用循环完整的呈现了全部过程。

     递归方法是一路往下走,到最底层只剩一个数归并,返回上一层有两个数,两两归并,依次往上返回。这里非递归相当于是反过来了,最开始一一归并,然后将有序的两两归并.......

    大体思路简单,但是控制边界值挺麻烦,先实现大框架:

    我们要一开始一一归并,再两两归并.......可以设定一个gap,让它一开始为1,每归并完一层就*2,

    就可以实现控制了。

    1. void MergeSortNon(int* a, int n)
    2. {
    3. int* tmp = (int*)malloc(sizeof(int) * n);
    4. if (tmp == NULL)
    5. {
    6. perror("malloc fail");
    7. return;
    8. }
    9. int gap = 1;
    10. while (gap < n)
    11. {
    12. for (int j = 0; j < n; j += gap * 2)
    13. {
    14. int i = j;
    15. int begin1 = j, end1 = j + gap - 1,
    16. begin2 = j + gap, end2 = j + 2 * gap - 1;
    17. while (begin1 <= end1 && begin2 <= end2)
    18. {
    19. if (a[begin1] <= a[begin2])
    20. {
    21. tmp[i++] = a[begin1++];
    22. }
    23. else
    24. {
    25. tmp[i++] = a[begin2++];
    26. }
    27. }
    28. while (begin1 <= end1)
    29. {
    30. tmp[i++] = a[begin1++];
    31. }
    32. while (begin2 <= end2)
    33. {
    34. tmp[i++] = a[begin2++];
    35. }
    36. gap *= 2;
    37. memcpy(a, tmp, sizeof(int) * n);
    38. }
    39. }
    40. free(tmp);
    41. tmp = NULL;
    42. }

    注意这里begin1 ,end1,begin2,end2的初始值。对照着图可以推出来。

    这是基本框架,但是注意:上面数组里面个数正好为8,是2^3,当数组个数不是2^n时,情况就完全不同了。

    比如个数为9,10时,如图:

     比如这样两种情况就会出现问题。

     以左图为例,对照代码看:当gap = 4时,j = 8,begin2 = 9,end2 = 9 越界了。

    同理,右边也一样,其他情况下也会产生越界。为了方便观察什么情况下会越界,我们加入打印。

    1. int i = j;
    2. int begin1 = j, end1 = j + gap - 1,
    3. begin2 = j + gap, end2 = j + 2 * gap - 1;
    4. printf("[%d][%d],[%d][%d] ", begin1, end1, begin2, end2);

     当数组元素个数为2^n 时

      当数组元素个数为奇数个时

     当数组元素个数是偶数个但不为2^n 时

    根据上图,我们把越界情况分为三种

    1、第一组end1越界

    2、第二组begin2,end2全部越界

    3、第二组end2越界

    针对上述三种情况进行修正,就可以避免越界实现非递归。

    如果拷贝的地方是像上面一样全部归并完了再拷贝的,那就比较麻烦了。因为中间可能会拷回去越界的随机值,所以需要一一修正边界,不推荐这种拷贝方式。

    可以将拷贝放到循环里,归并多少数据就拷贝多少数据,就不会产生越界拷贝随机数的情况了,也不需要一一修正边界。

    如果是第一组end1越界,那他后面也不需要归并了,直接break出去就行。

    如果是第二组begin2,end2全部越界,那也是一样,说明不需要归并,也直接break即可。

    如果是第二组end2越界,那end2前面一个数还需要归并,此时修正一下end2,改成n-1。

    1. void MergeSortNon(int* a, int n)
    2. {
    3. int* tmp = (int*)malloc(sizeof(int) * n);
    4. if (tmp == NULL)
    5. {
    6. perror("malloc fail");
    7. return;
    8. }
    9. int gap = 1;
    10. while (gap < n)
    11. {
    12. for (int j = 0; j < n; j += 2 * gap)
    13. {
    14. int begin1 = j, end1 = j + gap - 1,
    15. begin2 = j + gap, end2 = j + 2 * gap - 1;
    16. int i = j;
    17. //第一组end1越界
    18. if (end1 >= n)
    19. {
    20. printf("[%d][%d]", begin1, n - 1);
    21. break;
    22. }
    23. //第二组全部越界
    24. if (begin2 >= n)
    25. {
    26. printf("[%d][%d]", begin1, end1);
    27. break;
    28. }
    29. //第二组end2越界
    30. if (end2 >= n)
    31. {
    32. printf("[%d][%d]", begin2, n - 1);
    33. //修正end2,继续归并
    34. end2 = n - 1;
    35. }
    36. printf("[%d][%d],[%d][%d] ", begin1, end1, begin2, end2);
    37. while (begin1 <= end1 && begin2 <= end2)
    38. {
    39. if (a[begin1] <= a[begin2])
    40. {
    41. tmp[i++] = a[begin1++];
    42. }
    43. else
    44. {
    45. tmp[i++] = a[begin2++];
    46. }
    47. }
    48. while (begin1 <= end1)
    49. {
    50. tmp[i++] = a[begin1++];
    51. }
    52. while (begin2 <= end2)
    53. {
    54. tmp[i++] = a[begin2++];
    55. }
    56. //归并哪一段,拷贝哪一段
    57. memcpy(a + j, tmp + j, sizeof(int) * (end2 - j + 1));
    58. }
    59. printf("\n");
    60. gap *= 2;
    61. }
    62. free(tmp);
    63. tmp = NULL;
    64. }

    为了方便观察,这里添加几个打印

     是不是和之前画图分析的过程一样呢。这就是非递归的玩法。

    如果是一次拷贝的修正边界值,麻烦一点,大家可以去尝试一下。

  • 相关阅读:
    mysql服务器无法启动问题处理
    风控——利用决策树挖掘策略规则
    矩阵分析与应用-6.2-奇异值分解-Section1
    C# WPF监听USB插入拨出
    Metasploit——渗透攻击模块(Exploit)
    Command 实用小工具
    Harmony os Next——Ble蓝牙模块
    跟我学企业级flutter项目:简化框架demo参考
    正则表达式30分钟入门教程
    「图像 cv2.seamlessClone」无中生有制造数据
  • 原文地址:https://blog.csdn.net/SAKURAjinx/article/details/126788060