• 【C语言】归并排序和计次排序


    一、归并排序

            归并排序要额外多开一个数组用于存放归并后的数据,然后再把排序好的数据放回原数组。

            归并排序使用递归的写法,大致分位两个步骤,一个是分解,一个是归并。而归并的数据必须是有序的才行,那么如何保证其最开始是有序的呢?

            其实这里进入递归到了最底层,其数据被分为单一个数为一个区间的时候,这里就认为其是有序的。

            分解会把数据分解为两个区间,然后做比较,如果是升序的话,一个个比较,把较小的放到临时数组。

            此时此段区间就变为有序了,返回上一层这段区间就又可以用来被归并。

    简单讲解归并流程,假设这里数组已经被分解为了两个有序的区间:

            这里就要依次进行比较然后把较小的数据放到临时数组。把数据放下去以后,i 往前走一步,然后再进行比较。

             

            这里2<4,把2放到下面的数组,然后 j 往前走一位。

            依次走到最后数组数据就排序好了,但是这里要注意一个情况,这种比较的方法固定要有一个区间先走完,这里左区间先走完了,但是右区间还剩一个数,也没办法比较了。

            所以为了防止这个情况,在归并结束后,如果哪个区间还有数据,就直接加到数组后面就可以,因为排序的数组是有序的,所以直接加到数组后面不会出错。

     

             

     代码:

    1. void _MergeSort(int* a, int begin, int end, int* temp)
    2. {
    3. if (begin >= end) //至少要有两个区间
    4. return;
    5. int mid = (begin + end) / 2;
    6. //分治 [beging,mid] [mid+1,end]
    7. _MergeSort(a ,begin,mid,temp);
    8. _MergeSort(a, mid+1, end, temp);
    9. int begin1 = begin,end1 = mid;
    10. int begin2 = mid + 1,end2 = end;
    11. int i = begin1; //这里注意是beging1
    12. //归并 把小的放到temp数组中去
    13. while (begin1 <= end1 && begin2 <= end2) {
    14. if (a[begin1]<a[begin2]) {
    15. temp[i++] = a[begin1++];
    16. }
    17. else {
    18. temp[i++] = a[begin2++];
    19. }
    20. }
    21. //如果一个数组走完了,还剩有数据,全部放到数组后面去
    22. while (begin1<=end1) {
    23. temp[i++] = a[begin1++];
    24. }
    25. while (begin2<=end2) {
    26. temp[i++] = a[begin2++];
    27. }
    28. //把数据拷贝回原数组
    29. memcpy(a + begin, temp + begin,(end-begin+1)*sizeof(int));
    30. }
    31. //递归实现
    32. void MergeSort(int *a,int n)
    33. {
    34. int* temp = (int*)calloc(n, sizeof(int));
    35. if (temp==NULL){
    36. perror("tmp初始化失败\n");
    37. return;
    38. }
    39. _MergeSort(a, 0, n-1, temp);
    40. free(temp);
    41. }

    测试:

            归并排序的时间复杂度是典型的:O(N*logN );

            

    二、归并排序非递归实现

            归并的非递归直接可以在数组中处理。

            其思路是把数组中的数进行分组,设gap个数为一组,每组数又有两个区间,这两个区间做比较,归并成一个有序的区间。有序的区间又可以分组,然后归并。

           

            gap=1,每单个一个数分为一组,归并后刚好变为gap=2,两个数的组。依次归并,最后整个数组就有序了。

    1. //归并排序的非递归
    2. void MergeSortNonR(int* a, int n)
    3. {
    4. int* temp = (int*)calloc(n, sizeof(int));
    5. if (temp == NULL) {
    6. perror("tmp初始化失败\n");
    7. return;
    8. }
    9. int gap = 1;
    10. while (gap < n) {
    11. for (int i = 0; i < n; i += 2*gap) { //注意这里是一次跳两组
    12. //[i][i+gap-1] [i+gap][i+2*gap-1]
    13. int begin1 = i, end1 = i + gap - 1;
    14. int begin2 = i + gap, end2 = i + 2 * gap - 1;
    15. int j = begin1;
    16. while (begin1 <= end1 && begin2 <= end2)
    17. {
    18. if (a[begin1] < a[begin2]) {
    19. temp[j++] = a[begin1++];
    20. }
    21. else {
    22. temp[j++] = a[begin2++];
    23. }
    24. }
    25. while (begin1 <= end1)
    26. {
    27. temp[j++] = a[begin1++];
    28. }
    29. while (begin2 <= end2) {
    30. temp[j++] = a[begin2++];
    31. }
    32. }
    33. //这里每个数据都归并了所以可以全部拷贝回去。
    34. memcpy(a, temp, sizeof(int) * n);
    35. gap *= 2;
    36. }
    37. free(temp);
    38. }


            思路比较好理解,但是要注意的是边界的控制。如果数组的长度不为2的次方倍数,那么不控制边界会造成越界。

            比如一个数组长度等于9,调试的时候这里打印一下区间。

             这里就会发现,有点地方的区间大于了数组的长度(数组下标是0-8,9也越界了)。除了begin1不会越界以外,其他的边界都会越界。

             所以这里为了防止边界越界,要控制一下边界。

    1. //归并排序的非递归
    2. void MergeSortNonR(int* a, int n)
    3. {
    4. int* temp = (int*)calloc(n, sizeof(int));
    5. if (temp == NULL) {
    6. perror("tmp初始化失败\n");
    7. return;
    8. }
    9. int gap = 1;
    10. while (gap < n) {
    11. printf("gap=%d-->",gap);
    12. for (int i = 0; i < n; i += 2*gap) { //注意这里是一次跳两组
    13. //[i][i+gap-1] [i+gap][i+2*gap-1]
    14. int begin1 = i, end1 = i + gap - 1;
    15. int begin2 = i + gap, end2 = i + 2 * gap - 1;
    16. //修正边界
    17. if (end1 >= n) { //end1越界
    18. end1 = n - 1;
    19. begin2 = n;
    20. end2 = n - 1;
    21. }
    22. else if (begin2 >= n) { //begin2越界
    23. begin2 = n;
    24. end2 = n - 1;
    25. }
    26. else if (end2 >= n) { //end2越界
    27. end2 = n - 1;
    28. }
    29. //打印边界:
    30. printf("--[%d][%d],[%d][%d]",begin1,end1,begin2,end2);
    31. int j = begin1;
    32. while (begin1 <= end1 && begin2 <= end2)
    33. {
    34. if (a[begin1] < a[begin2]) {
    35. temp[j++] = a[begin1++];
    36. }
    37. else {
    38. temp[j++] = a[begin2++];
    39. }
    40. }
    41. while (begin1 <= end1)
    42. {
    43. temp[j++] = a[begin1++];
    44. }
    45. while (begin2 <= end2) {
    46. temp[j++] = a[begin2++];
    47. }
    48. }
    49. //这里每个数据都归并了所以可以全部拷贝回去。
    50. memcpy(a, temp, sizeof(int) * n);
    51. printf("\n");
    52. gap *= 2;
    53. }
    54. free(temp);
    55. }

    PS:越界后设 begin2 < end2 ,这样越界的右区间就不会进入归并的步骤了。

    测试: 

            

            

            还有一种思路,如果区间发生数组越界,那么就不进行归并了。

    1. //归并排序的非递归 第二个控制边界的办法
    2. void MergeSortNonR_Test(int* a, int n)
    3. {
    4. int* temp = (int*)calloc(n, sizeof(int));
    5. if (temp == NULL) {
    6. perror("tmp初始化失败\n");
    7. return;
    8. }
    9. int gap = 1;
    10. while (gap < n) {
    11. printf("gap=%d-->", gap);
    12. for (int i = 0; i < n; i += 2 * gap) { //注意这里是一次跳两组
    13. //[i][i+gap-1] [i+gap][i+2*gap-1]
    14. int begin1 = i, end1 = i + gap - 1;
    15. int begin2 = i + gap, end2 = i + 2 * gap - 1;
    16. //如果end1、begin2越界就不归了
    17. if (end1 >= n || begin2 >= n) {
    18. break;
    19. }
    20. else if (end2 >= n) { //end2越界还是要归并
    21. end2 = n - 1;
    22. }
    23. int m = end2 - begin1 + 1;
    24. //打印边界:
    25. printf("--[%d][%d],[%d][%d]", begin1, end1, begin2, end2);
    26. int j = begin1;
    27. while (begin1 <= end1 && begin2 <= end2)
    28. {
    29. if (a[begin1] < a[begin2]) {
    30. temp[j++] = a[begin1++];
    31. }
    32. else {
    33. temp[j++] = a[begin2++];
    34. }
    35. }
    36. while (begin1 <= end1)
    37. {
    38. temp[j++] = a[begin1++];
    39. }
    40. while (begin2 <= end2) {
    41. temp[j++] = a[begin2++];
    42. }
    43. //注意如果不归并了,这里不能把数据拷贝回去,不然会把未初始化的值拷贝回去,所以只能拷贝已经归并了的数据。
    44. memcpy(a + i, temp + i, sizeof(int) * m);
    45. }
    46. printf("\n");
    47. gap *= 2;
    48. }
    49. free(temp);
    50. }

    PS:如果是区间越界就跳过归并的话,这里要注意就不能把全部的数据都拷贝回原来的数组,因为没发生归并,这里临时数组里面是未初始化的值,拷贝回去会造成覆盖。所以这里要注意,这里拷贝的话只能拷贝已经归并了的数据。

    三、计次排序

            计次排序也要额外申请一个数组,其数组大小是原数组最大元素和最小元素的差值。其存的数据是原数组里面数据出现的次数。

            然后按照出现的次数把数据写回原来的数组。此排序是个非比较排序。

            

            

      代码:

    1. //计次循环
    2. void CountSort(int *a,int n)
    3. {
    4. //选出最大最小的数,计算差值
    5. int max=a[0], min=a[0];
    6. for (int i = 0; i < n;i++) {
    7. if (a[i]>max) {
    8. max = a[i];
    9. }
    10. if (a[i]<min) {
    11. min = a[i];
    12. }
    13. }
    14. int rang = max - min+1;
    15. int* count = (int*)calloc(rang,sizeof(int));
    16. if (count==NULL) {
    17. perror("count为NULL");
    18. exit(-1);
    19. }
    20. for (int i = 0; i < n;i++) {
    21. //记录出现数据的次数
    22. count[a[i] - min]++;
    23. }
    24. //放回原数组
    25. int k = 0;
    26. for (int j = 0; j < rang;j++) {
    27. while (count[j]--) {
    28. a[k++] = j + min;
    29. }
    30. }
    31. free(count);
    32. }

    测试:

           

    局限性:

            1.浮点数、字符串不能排序。

            2.数据范围大,空间复杂度会很高。

            此排序的性质就决定了它只能排序范围比较小,重复度较高的数据。如果数据范围交到,其时间和空间的消耗都比较多。

    时间复杂度: O( max(rang,N) )        

    空间复杂度: O(rang)        //最小值和最大值的差值越大约消耗空间


  • 相关阅读:
    【力扣10天SQL入门】Day2
    96. 不同的二叉搜索树
    【ESP 保姆级教程】疯狂Node.js服务器篇 ——钉钉/微信/飞书报警从Arduino移植到NodeJs服务器实现
    【论文阅读】MAG:一种用于航天器遥测数据中有效异常检测的新方法
    实战:大数据Flink CDC同步Mysql数据到ElasticSearch
    力扣 寻找旋转排序数组中的最小值 二分
    幸福消费成酒店投资趋势红利,荟语酒店凭何打造品牌核心优势
    【React Hooks&Redux--学习小结】
    JVM执行流程
    qt 使用qt_material库打包报错 ImportError: cannot import name ‘uic‘ from ‘PyQt5‘
  • 原文地址:https://blog.csdn.net/weixin_45423515/article/details/125627853