当前位置:   article > 正文

MergeSort(迭代归并排序)——C语言实现_mergesort函数c语言

mergesort函数c语言

前言:

归并排序跟快速排序有异曲同工之妙,都是分治法的典型代表。但是这种分治法都有不小的弊端,就是需要占用大量的系统栈,很容易造成空间的大量浪费,所以就有用迭代来优化递归的操作。这次的归并排序,我采用迭代的方式来进行编写。

思路:

归并算法如其名,需要不断地归并。我们在学习数组的时候,应该学习到过合并两个有序的数组的时间复杂度是很低的,不断地二分并进行合并,这个算法的时间复杂度仅为O(nlogn),但是合并数组的时候是需要一个额外的相当于原数组大小的空间的,所以空间复杂度并不是很好O(n)

所以最先的最先,我们要先开辟一块空间

  1. int *b = (int*)malloc((length+5) * sizeof(int));
  2. //加上一些偏移量以防越界,同时,要记得最后要free掉这片空间
  3. //这个free不重视要出大问题的,接下来会说

开始写这个排序最先想到的莫非就是如何不断地二分。既然我们选择的是自下而上的进行迭代,我们一开始的1个1个的比较从而形成2个,2个比较的情况,因此我们的两层循环其实就已经都定下来了:(offset是偏移量的意思)

  1. for (offset = 1; offset < length; offset *=2)
  2. for (left = 0; left < length; left += 2 * offset)

由于是二分,所以偏移量自下而上的是1,2,4,8···,也就是说 偏移量的变化是  offset *=2

然后left和接下来的right是在原数组中进行滑动的下标,用来比较的。

接下来,我们需要思考一下left 和right有哪些情况。其实,这里主要的问题就是怕越界,因此我们为了方便表示,重新定义三个变量,来处理可能越界的问题:

  1. int low = left;
  2. int mid = min(left + offset, length);
  3. int high = min(left + 2 * offset, length);

(left+offset 相当于右比较框的第一个元素;left+2*offset 相当于同样长的第三个比较框的第一个元素)

这里的mid,是左边比较框的最右端再往右一个单位,因此可能会超出数组长度,需要在len和left+offset 中选择较小的一个;

这里的mid,同时也是右边比较框的第一个元素,high是右边比较框的最右端再往右一个单位,与上面同理,有可能越界,所以在left + 2 * offset 和 len 选择较小的一个。这样,再为了方便重新定义几个量来表示,就很清晰明了了:

  1. int left1 = low, right1 = mid;
  2. int left2 = mid, right2 = high;

接下来的部分其实比较简单,就是两个有序数组进行比较,并归并入新数组的过程:

  1. //确定指针没跑到下一个区间去
  2. while (left1 < right1 && left2 < right2)
  3. b[i++] = a1[left1] < a1[left2] ? a1[left1++] : a1[left2++];
  4. //下头两个while只执行一个,把剩下的元素全搬进去
  5. while (left1 < right1)
  6. b[i++] = a1[left1++];
  7. while (left2 < right2)
  8. b[i++] = a1[left2++];

当整个数组比较完以后,我们开辟的b数组中,就是合并过一次的数组了,我们需要把a和b换个位置。

  1. int* temp = a1;
  2. a1 = b;
  3. b = temp;

至此呢,大概已经完成了,但是,不注意free要出现的大问题来了:

如果上面那个交换的代码交换了奇数次,如果我们free(b),我们就会free一片静态内存了,这是不被允许的。如下图所示:

 为了解决这个问题,我们一开始就需要定义指针来指向空间,最后用来做校验。当然,释放的时候我们为了保险直接释放b1就行了,绝对没问题。

 这边我们应该这么写:

  1. if (b != b1)
  2. {
  3. for (int i = 0; i < length; i++)
  4. {
  5. a[i] = a1[i];
  6. }
  7. }
  8. free(b1);

肯定有人会跟我有一样的问题,为什么不能再改变指针的位置直接成功呢?就像下面一样:

  1. if (b != b1)
  2. {
  3. a = a1;
  4. }
  5. free(b1);

这个时候我们用地址找一下,就很容易明白了:

一开始的地址:

处理完的地址:

 

 我们会发现其实他所有的地址全都指向了一开始开辟的,最后要被释放掉的动态区域b!

那么为什么编译器不报错呢?这里应该和我在别处了解到的别的一样:编译器会做一定的保留。

所以并不会直接报错,但是会出现意料之外的情况。因此,我们不能改动地址,而应该用赋值操作,来使a数组,与a1数组同步(像我上面讲的,a1一定是有序的那一个,而我们最后需要的是a数组)

核心代码:

这边的核心代码为了阅读以及复制方便,我就只保留 归并排序 的内容了,我的一些试错及写出来的体验和心得就放在下面的测试代码中了,想具体了解的,可以移步到下面了。

  1. void MergeSort(int *a,int length)
  2. {
  3. int* a1 = a;
  4. int *b = (int*)malloc((length+5) * sizeof(int));
  5. int* b1 = b;
  6. int offset;//offset是每次归并的两个小数组的偏移量
  7. for (offset = 1; offset < length; offset *=2)
  8. {
  9. int left;
  10. int i=0; //i是给新数组准备的下标
  11. for (left = 0; left < length; left += 2 * offset)
  12. {
  13. int low = left, mid = min(left + offset, length), high = min(left + 2 * offset, length);
  14. int left1 = low, right1 = mid;
  15. int left2 = mid, right2 = high;
  16. while (left1 < right1 && left2 < right2)
  17. b[i++] = a1[left1] < a1[left2] ? a1[left1++] : a1[left2++];
  18. while (left1 < right1)
  19. b[i++] = a1[left1++];
  20. while (left2 < right2)
  21. b[i++] = a1[left2++];
  22. }
  23. int* temp = a1;
  24. a1 = b;
  25. b = temp;
  26. }
  27. if (b != b1)
  28. {
  29. for (int i = 0; i < length; i++)
  30. {
  31. a[i] = a1[i];
  32. }
  33. }
  34. free(b1);
  35. }

整体测试代码:

  1. #define _CRT_SECURE_NO_WARNINGS
  2. #include<stdio.h>
  3. #include<stdlib.h>
  4. #include<time.h>
  5. #define N 30
  6. //之前写过了,就不再全写一遍了
  7. void generate_random_number(int*, int, int);
  8. void swap(int*, int*);
  9. //自下而上迭代
  10. void MergeSort(int *a,int length)
  11. {
  12. int* a1 = a;//如果觉得这一步是多余的,那xd你和我一开始觉得一样,但其实这个是必要的!往下看。
  13. int *b = (int*)malloc((length+5) * sizeof(int));
  14. int* b1 = b;
  15. int offset;//offset是每次归并的两个小数组的偏移量
  16. for (offset = 1; offset < length; offset *=2)
  17. {
  18. int left;
  19. int i=0; //i是给新数组准备的下标
  20. //left是每次进行归并的左边小数组,right是右边的;每次归并完两个,整体向右挪动两倍的offset
  21. //注释掉的是写的错的代码。错在没有考虑数组指针越界的情况,其实也就一行 right = min(left+offset,length)
  22. //for (left = 0,right = left + offset,i = 0; left < length && right < length; left += offset * 2, right = left + offset)
  23. //{
  24. // int left1 = left;
  25. // int right1 = right;
  26. // while (right1 < left + 2*offset && left1 < right)//指针没跑到下一个区间去
  27. // {
  28. // if (a[left1] <= a[right1])
  29. // b[i++] = a[left1++];
  30. // else
  31. // b[i++] = a[right1++];
  32. // }
  33. // //下头两个while只执行一个,把剩下的元素全搬进去
  34. // while (left1 < right)
  35. // b[i++] = a[left1++];
  36. // while(right1 < left + 2 * offset)
  37. // b[i++] = a[right1++];
  38. //}
  39. for (left = 0; left < length; left += 2 * offset)
  40. { //下这两步都是为了确保数组下标不会越界
  41. int low = left, mid = min(left + offset, length), high = min(left + 2 * offset, length);
  42. int left1 = low, right1 = mid;
  43. int left2 = mid, right2 = high;
  44. //确定指针没跑到下一个区间去
  45. while (left1 < right1 && left2 < right2)
  46. b[i++] = a1[left1] < a1[left2] ? a1[left1++] : a1[left2++];
  47. //下头两个while只执行一个,把剩下的元素全搬进去
  48. while (left1 < right1)
  49. b[i++] = a1[left1++];
  50. while (left2 < right2)
  51. b[i++] = a1[left2++];
  52. }
  53. int* temp = a1;
  54. a1 = b;
  55. b = temp;
  56. /* 不容易,我终于想懂了!
  57. 上面这一步实现交换,使得有序的序列肯定是a所指的地址,但是最后a的地址不一定能回到arr,可能在b上
  58. 但是,free函数只能释放动态开辟的内存,也就是只能释放原先b所指的内存空间,如果释放到a,直接严重弹框报错
  59. 外头我画了张图
  60. 根据log2(length)算出来的不同值,下面的交换步骤可能只进行奇数次,因此还需要多一些步骤才可以完全实现
  61. 如果只进行奇数次的话,我函数传参时传入的虽然是真实地址,但是他指向了我在函数中开辟的临时堆空间。
  62. 这块堆空间在函数进行完以后会释放掉,因此如果进行奇数次的话,我们在函数结束以后,原函数的地址是一片未知区域!
  63. 如果确定只进行偶数次,是可以不要下面步骤的
  64. */
  65. }
  66. /*
  67. 上面的解释也告诉我们,为什么要在函数开头看似无用的新建一个a变量和b1指向我们传入的和创建的地址,也是我们在最后要保证能找到我们一开始的地址
  68. 记录了以后我们可以简便的使用 b != b1 来进行判断。
  69. */
  70. if (b != b1)
  71. {
  72. for (int i = 0; i < length; i++)
  73. {
  74. a[i] = a1[i];
  75. }
  76. }
  77. free(b1);
  78. }
  79. int main()
  80. {
  81. int arr[N + 10] = { 0 };
  82. generate_random_number(arr, 0, 1024);
  83. MergeSort(arr,N);
  84. printf("MergeSort排序后数列:\n");
  85. for (int i = 0; i < N; i++)
  86. printf("%d ", arr[i]);
  87. printf("\n");
  88. return 0;
  89. }

测试结果:

至此,归并排序完成

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/知新_RL/article/detail/817401
推荐阅读
相关标签
  

闽ICP备14008679号