• 归并排序和计数排序


    排序主要有八种,前面6种在之前已经介绍并实现过了。

    这是前面的6大牌排序介绍:http://t.csdn.cn/l06fT

    归并排序:

    基本思想:归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有 序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。

    下面让我们模拟实现归并排序:

    实现的方式是用一个临时数组去存储归并后的数据,然后再拷贝回去,如果直接在原数组去实现的话,就需要进行数据的移动,这样很可能就会导致数据的丢失。归并排序递归实现和二叉树的后序遍历有点像,都是访问都最后,然后出来的左右两组数据之后再递归的根的位置。

    1. void _MergeSort(int* a, int begin, int end, int* tmp)
    2. {
    3. if (begin >= end)
    4. {
    5. return;
    6. }
    7. int mid = (begin+end)/2;
    8. //直接递归到最底,归并排序相当于二叉树的后序遍历
    9. _MergeSort(a, begin, mid, tmp);
    10. _MergeSort(a, mid+1, end , tmp);
    11. int begin1 = begin; int end1 = mid;
    12. int begin2 = mid + 1; int end2 = end;
    13. int i = begin;
    14. while (begin1 <= end1 && begin2 <= end2)
    15. {
    16. if (a[begin1] <= a[begin2])
    17. {
    18. tmp[i++] = a[begin1++];
    19. }
    20. else
    21. {
    22. tmp[i++] = a[begin2++];
    23. }
    24. }
    25. while (begin1 <= end1)
    26. {
    27. tmp[i++] = a[begin1++];
    28. }
    29. while (begin2 <= end2)
    30. {
    31. tmp[i++] = a[begin2++];
    32. }
    33. //复制回原数组
    34. memcpy(a + begin, tmp + begin, (end2 - begin+1) * sizeof(int));
    35. }
    36. void MergeSort(int* a, int n)
    37. {
    38. //创建临时数组
    39. int* tmp = (int*)malloc(n * sizeof(int));
    40. if (tmp == NULL)
    41. {
    42. perror("malloc fail");
    43. return;
    44. }
    45. _MergeSort(a, 0, n - 1, tmp);
    46. free(tmp);
    47. tmp = NULL;
    48. }

     归并排序并不像快排一样,快排的递归深度为O(N),所以递归的方法对于快排来说用处不大,所以快排通常使用非递归的方式去解决,也就是用队列来模拟递归的过程,对于归并排序来说它的递归深度并不高,是log(N),所以使用递归去实现归并排序也是没有问题。但这里也会实现一下归并排序的非递归方式

    归并排序的非递归方式:
    对于归并排序的非递归方式,我们不能使用简单的栈或者队列来模拟实现递归的过程,因为递归的过程像后序遍历,但是我们可以使用循环的方式去模拟实现,就像之前所学过的斐波那契数列一样,也可以使用循环的方式直接实现。

    思路:我们第一次可以一个一个分开,然后两个两个进行归并,然后再4个4个进行归并。当然这其中有些特殊情况,我们需要处理一下。

    特殊情况及其出来方式:
    1.第一组数据的部分缺失:

     2.第二组数据全部缺失:

    3.第二组数据部分缺失:

     以上3种情况都可能出现,可以分开出来,那为什么第二组全部缺失要放在第二组部分缺失前面呢?因为全部缺失是部分缺失的特例,而且它们的处理方式是不一样的,所以必须先单独出来,不然全部缺失的特例就没有办法解决了。

    1. void MergeSortNonR(int* a, int n)
    2. {
    3. int* tmp = (int*)malloc(n * sizeof(int));
    4. if (tmp == NULL)
    5. {
    6. perror("malloc fail");
    7. return;
    8. }
    9. int gap = 1;
    10. while (gap < n)
    11. {
    12. //gap个数据和gap个数据进行排序
    13. for (int j = 0; j < n; j += 2 * gap)
    14. {
    15. int begin1 = j; int end1 = j + gap-1;
    16. int begin2 = j + gap; int end2 = j + 2 * gap - 1;
    17. int i = j;
    18. //第一组数据部分缺失
    19. if (end1 >= n)
    20. {
    21. break;
    22. }
    23. //第二组数据全部缺失
    24. if (begin2 >= n)
    25. {
    26. break;
    27. }
    28. //第二组数据部分缺失
    29. if (end2 >= n)
    30. {
    31. //修正一下
    32. end2 = n - 1;
    33. }
    34. while (begin1 <= end1 && begin2 <= end2)
    35. {
    36. if (a[begin1] <= a[begin2])
    37. {
    38. tmp[i++] = a[begin1++];
    39. }
    40. else
    41. {
    42. tmp[i++] = a[begin2++];
    43. }
    44. }
    45. while (begin1 <= end1)
    46. {
    47. tmp[i++] = a[begin1++];
    48. }
    49. while (begin2 <= end2)
    50. {
    51. tmp[i++] = a[begin2++];
    52. }
    53. //每次交换一组都拷贝回原数组
    54. memcpy(a + j, tmp + j, (end2 - j + 1) * sizeof(int));
    55. }
    56. gap *= 2;
    57. }
    58. free(tmp);
    59. tmp = NULL;
    60. }

    归并排序的特性总结:

    1. 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。

    2. 时间复杂度:O(N*logN)

    3. 空间复杂度:O(N)  , 使用递归的方式也能较好的解决,因为递归的深度并不高

    4. 稳定性:稳定

    非比较排序之一计数排序:

    非比较排序意思是不使用比较的方式进行排序,其中计数排序是其中比较有用的排序。

    但是计数排序有一定的局限性:

    首先先介绍计数排序的原理:

    思想:计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。 操作步骤: 1. 统计相同元素出现次数 2. 根据统计的结果将序列回收到原来的序列中

     下面介绍绝对映射和相对映射:

     根据上面的分析,我们不难发现,计数排序是有一定的局限性的,就是所排的数据要相对集中,否则浪费的空间就会很多。

    实现的方式:

    首先我们要先计算出我们要排的最大和最小值,然后算出他们的差值,因为这个是左闭右闭区间,所以要加一才能计算出接下来要开辟数组的大小。然后遍历数组统计出他们分别出现的次数,并保存到tmp数组中,最后把tmp数组的数据进行还原即可。

    下面是代码实现:

    1. void CountSort(int* a, int n)
    2. {
    3. //计数排序首先要找到最大和最小的相减得到新的数组的个数
    4. int max = a[0]; int min = a[0];
    5. for (int i = 0; i < n; i++)
    6. {
    7. if (a[i] > max)
    8. {
    9. max = a[i];
    10. }
    11. if (a[i] < min)
    12. {
    13. min = a[i];
    14. }
    15. }
    16. //因为这里是左闭右闭的区间,所以要加一
    17. int range = max - min + 1;
    18. int* tmp = (int*)malloc(range * sizeof(int));
    19. memset(tmp, 0, range * sizeof(int));
    20. for (int i = 0; i < n; i++)
    21. {
    22. tmp[a[i] - min]++;
    23. }
    24. //将tmp数组的元素赋值回原数组
    25. int j = 0;
    26. for (int i = 0; i < range; i++)
    27. {
    28. while ((tmp[i])--)
    29. {
    30. a[j] = i + min;
    31. j++;
    32. }
    33. }
    34. free(tmp);
    35. tmp = NULL;
    36. }

    最后总结计数排序:虽然计数排序的时间复杂度是O(N),但是它的使用收到很多的限制,它只适合整形的数据,同时数据之间的差值还要比较小才适用,否则开辟的空间会非常大。

    排序算法的总结:

    排序中运用最多的是快排。为什么我们要关注排序的稳定性,因为有时的数据要保证数据相对比较有序,例如:在高考都是600分的时候,我们可能就需要按照语文或者其他学科的成绩进行排名。所以我们最好是要保证数据相对稳定。

     对于非比较排序来说使用的并不多。

  • 相关阅读:
    代码随想录1刷—链表篇
    DID革命:详解PoP、SBT和VC三种去中心化身份方案
    C嘎嘎 - 类和对象
    将 Cloudflare 页面与 IPFS 结合使用
    Redisson--最好用的Redis客户端--介绍
    零基础入门JavaWeb——Web基本概念
    问题记录v(●‘◡‘●)v
    PHP会话技术session我不允许还有人不会!
    队列(C语言实现)
    面向计算思维培养的PBL教学模式设计以模式识别课程为例
  • 原文地址:https://blog.csdn.net/weixin_72068014/article/details/126810369