0


排序算法之非比较排序(基数排序和计数排序)

**个人主页:欢迎大家光临——>**沙漠下的胡杨

** 各位大帅哥,大漂亮**

如果觉得文章对自己有帮助

可以一键三连支持博主

** 你的每一分关心都是我坚持的动力**

** **

☄:本期重点:

** 希望大家每天都心情愉悦的学习工作。**


在我们之前的讲解中,我们学到了大多数的比较排序,今天我们主要学一下非比较排序。

计数排序:

  1. 计数排序是一个非基于比较的排序算法,该算法于1954年由 Harold H. Seward 提出。它的优势在于在对一定范围内的整数排序时,它的复杂度为Ο(n+k)(其中k是整数的范围),快于任何比较排序算法。 当然这是一种牺牲空间换取时间的做法,而且O(k)>O(n*log(n))的时候其效率反而不如基于比较的排序(基于比较的排序的时间复杂度在理论上的下限是O(n*log(n)), 如归并排序,堆排序)

排序思想:

1. 确定开辟的空间大小(一般为数据中的最大值 - 最小值的差值)。、

2. 确定对应关系。

3. 把数据对应关系的数组下标自增。

4. 根据队形关系拷贝数据。

计数排序图示分析:

首先我们有原数组,还要开辟一个辅助数组。并且辅助数组的大小是根据原数组中的数据的最大值—最小值决定的。下面数组中的值都在 1 到 8之间,所以我们辅助数组开辟 8 个大小的空间,其中 原数组中 1 对应辅助数组中下标 0 的位置,原数组中 8 数据,对应辅助数组下标为 7 的位置,刚开始时我们把辅助数据都为内存设置为0。

下面我们把数据根据对应关系写入辅助数组中去,1对应位置就是数组下标为0的位置,然后把辅助数组内的数据自增。8 对应下标为 7 的位置,然后把辅助数组内的数组自增。

写入完成后,我们根据辅助数组和原数组的对应关系,一个个的在写入原数组,其中辅助数组 0 下标对应的是 1 ,然后 0 下标的数据 2 表示写入两次 。同理,下标为辅助数组 7 下标对相应的是 8 ,辅助数组下标为 7 的数据为 2 ,表示写入 2 次 8。

代码实现:

  1. void CountSort(int* a, int n)//计数排序
  2. {
  3. //获取最大值和最小值
  4. int max = a[0];
  5. int min = a[0];
  6. for (int i = 0; i < n; i++)
  7. {
  8. if (max < a[i])
  9. {
  10. max = a[i];
  11. }
  12. if (min > a[i])
  13. {
  14. min = a[i];
  15. }
  16. }
  17. //获取开辟的区间大小
  18. int rang = max - min + 1;
  19. //开辟空间
  20. int* count = (int*)malloc(sizeof(int)*rang);
  21. if (count == NULL)
  22. {
  23. perror("malloc fail");
  24. exit(-1);
  25. }
  26. //把辅助数组的区间设置为0。
  27. memset(count, 0, sizeof(int)*rang);
  28. //根据对应关系写入辅助数组
  29. for (int i = 0; i < n; i++)
  30. {
  31. //写入时,根据对应关系一定是下标原数组数据减去最小值。
  32. count[a[i] - min]++;
  33. }
  34. //根据对应关系写回原数组
  35. int j = 0;
  36. //判断区间为是否结束
  37. for (int i = 0; i < rang; i++)
  38. {
  39. //此时判断的是辅助数组的数据是否为0
  40. while (count[i]--)
  41. {
  42. a[j++] = i + min;
  43. }
  44. }
  45. //释放空间
  46. free(count);
  47. count = NULL;
  48. }

基数排序:

基数排序算法思想:

** 基数排序(radix sort)属于“分配式排序”(distribution sort),又称“桶子法”(bucket sort)或bin sort,顾名思义,将要排序的元素分配至某些“桶”中,以达到排序的作用,基数排序法是属于稳定性的排序,其时间复杂度为O (nlog(r)m),其中r为所采取的基数,而m为堆数,在某些时候,基数排序法的效率高于其它的稳定性排序法。**

奇数排序实现流程:

1. 我们首先明确基数,一般是0~9这10个数字为基数。

2. 选取最大数的位数作为排序的次数。

3.然后根据个位上的数据排序,拷贝回原数组,再根据十位上的数据排序,拷贝回原数组(不足的位数补0,比如 2 ,2不到10,所以十位为 0 )

4.一直拷贝到最大数的最高位数停止,此时数据已经有序。

基数排序图示:

下面是一些随机数,然后我们排序,首先我们确定基数是 0~9 。然后我们确定最大数的最高位次是3,表示排序 3 次 。

下面我们按照个位上的数进行分配数据。

然后在进行回收数据,先放的数据先取出(先进先出):

根据十位上的数据分发数据:

回收十位上排序好的数据:

根据百位上的数据分发:

在回收百位上排序好的数据:

因为最大的数只有百位,所以此时排序完成。

代码实现(用队列辅助实现):

  1. //最大数据的位数,在数据量较小时,我们可以看出所以直接定义了最大的位数
  2. #define K 3
  3. //定义全局的10个队列,对应基数为0 ~ 9
  4. Queue q0,q1, q2, q3, q4, q5, q6, q7, q8, q9;
  5. //获取该数据第几位的数字
  6. int GetKey(int val, int k)
  7. {
  8. int keyi = 0;
  9. while (k >= 0)
  10. {
  11. keyi = val % 10;
  12. val /= 10;
  13. k--;
  14. }
  15. return keyi;
  16. }
  17. //分发数据
  18. void Distribute(int* a, int left, int right, int k)
  19. {
  20. for (int i = left; i < right; i++)
  21. {
  22. //获取当前要排序的位上的数据。
  23. int key = GetKey(a[i], k);
  24. //按照数据的每个位上的数字,分别进入对应的队列
  25. switch (key)
  26. {
  27. case 0:
  28. QueuePush(&q0, a[i]);
  29. break;
  30. case 1:
  31. QueuePush(&q1, a[i]);
  32. break;
  33. case 2:
  34. QueuePush(&q2, a[i]);
  35. break;
  36. case 3:
  37. QueuePush(&q3, a[i]);
  38. break;
  39. case 4:
  40. QueuePush(&q4, a[i]);
  41. break;
  42. case 5:
  43. QueuePush(&q5, a[i]);
  44. break;
  45. case 6:
  46. QueuePush(&q6, a[i]);
  47. break;
  48. case 7:
  49. QueuePush(&q7, a[i]);
  50. break;
  51. case 8:
  52. QueuePush(&q8, a[i]);
  53. break;
  54. case 9:
  55. QueuePush(&q9, a[i]);
  56. break;
  57. default:
  58. break;
  59. }
  60. }
  61. }
  62. //回收数据
  63. void Collect(int* a)
  64. {
  65. //下标值
  66. int i = 0;
  67. //依次取出0到9队列中的数据
  68. while (!QueueEmpty(&q0))
  69. {
  70. a[i++] = QueueFront(&q0);
  71. QueuePop(&q0);
  72. }
  73. while (!QueueEmpty(&q1))
  74. {
  75. a[i++] = QueueFront(&q1);
  76. QueuePop(&q1);
  77. }
  78. while (!QueueEmpty(&q2))
  79. {
  80. a[i++] = QueueFront(&q2);
  81. QueuePop(&q2);
  82. }
  83. while (!QueueEmpty(&q3))
  84. {
  85. a[i++] = QueueFront(&q3);
  86. QueuePop(&q3);
  87. }
  88. while (!QueueEmpty(&q4))
  89. {
  90. a[i++] = QueueFront(&q4);
  91. QueuePop(&q4);
  92. }
  93. while (!QueueEmpty(&q5))
  94. {
  95. a[i++] = QueueFront(&q5);
  96. QueuePop(&q5);
  97. }
  98. while (!QueueEmpty(&q6))
  99. {
  100. a[i++] = QueueFront(&q6);
  101. QueuePop(&q6);
  102. }
  103. while (!QueueEmpty(&q7))
  104. {
  105. a[i++] = QueueFront(&q7);
  106. QueuePop(&q7);
  107. }
  108. while (!QueueEmpty(&q8))
  109. {
  110. a[i++] = QueueFront(&q8);
  111. QueuePop(&q8);
  112. }
  113. while (!QueueEmpty(&q9))
  114. {
  115. a[i++] = QueueFront(&q9);
  116. QueuePop(&q9);
  117. }
  118. }
  119. void RadixSort(int* a, int left, int right)
  120. {
  121. //初始化队列
  122. QueueInit(&q0);
  123. QueueInit(&q1);
  124. QueueInit(&q2);
  125. QueueInit(&q3);
  126. QueueInit(&q4);
  127. QueueInit(&q5);
  128. QueueInit(&q6);
  129. QueueInit(&q7);
  130. QueueInit(&q8);
  131. QueueInit(&q9);
  132. for (int i = 0; i < K; i++)
  133. {
  134. //分发数据
  135. Distribute(a, left, right, i);
  136. //回收数据
  137. Collect(a);
  138. }
  139. //销毁队列
  140. QueueDestroy(&q0);
  141. QueueDestroy(&q1);
  142. QueueDestroy(&q2);
  143. QueueDestroy(&q3);
  144. QueueDestroy(&q4);
  145. QueueDestroy(&q5);
  146. QueueDestroy(&q6);
  147. QueueDestroy(&q7);
  148. QueueDestroy(&q8);
  149. QueueDestroy(&q9);
  150. }

因为我们使用纯C来写的,所以在一些代码上比较麻烦,需要注意一下几点:

1 . 队列要定义为全局的,方便我们直接使用,否则作为参数传递太过麻烦。

2. 我们可以直接写入要排的数据的最大位数,也可以写入的位数大一点,比如最大值只有百位的数据,我们可以写入千位,也就是比预定的要多排序一次,只会影响效率,不会影响结果。

3.使用队列的好处是我们不用考虑数据先进先出这点啦,因为这是队列的性质。

4.我们需要先写好一个队列才能使用上面的方法。

上述的写法十分麻烦,下面我们使用数组方式实现。

代码实现(数组实现):

  1. //求数据的最大值
  2. int GetMaxBitCount(int* array, int size)
  3. {
  4. //设定位数初始值
  5. int max_count = 1;
  6. //设定初始边界值
  7. int bound = 10;
  8. // 针对数组中的每一个元素, 都使用边界值来进行试探.
  9. // 看看边界值是否能够涵盖数组中的当前值.
  10. int i = 0;
  11. for (; i < size; ++i)
  12. {
  13. while (array[i] >= bound)
  14. {
  15. ++max_count;
  16. bound *= 10;
  17. }
  18. }
  19. //返回数据中最大的位数
  20. return max_count;
  21. }
  22. void RadixSort1(int* array, int size) {
  23. // 1. 求出最大的数字是多少位
  24. int max_bit_count = GetMaxBitCount(array, size);
  25. //开辟临时数组
  26. int* bucket = (int*)malloc(sizeof(int)* size);
  27. // 2. 最大的数字有多少位, 就需要进行多少次的排序
  28. int radix = 1; // 表示当前要取个位, 十位, 还是百位...
  29. int count = 0;
  30. for (; count < max_bit_count; ++count, radix *= 10)
  31. {
  32. // 统计每个桶中的元素个数(基数桶)
  33. int bucket_count[10] = { 0 };
  34. int i = 0;
  35. for (; i < size; ++i)
  36. {
  37. ++bucket_count[array[i] / radix % 10];
  38. }
  39. // 根据每个桶的元素的个数, 分配起始位置.(辅助数组)
  40. // 一维数组表示二维数组
  41. int start_addr[10] = { 0 };
  42. for (i = 1; i < 10; ++i)
  43. {
  44. start_addr[i] = start_addr[i - 1] + bucket_count[i - 1];
  45. }
  46. // 代码运行到这里, 就已经把地盘瓜分好了.
  47. // 接下来将所有的元素放到桶中
  48. for (i = 0; i < size; ++i)
  49. {
  50. int bucket_no = array[i] / radix % 10;
  51. // 这里的 start_addr[bucket_no]++ 相当于线性表的尾插.
  52. // 通过 start_addr[bucket_no] 记录每个桶当前写到了哪个位置.
  53. bucket[start_addr[bucket_no]++] = array[i];
  54. }
  55. memcpy(array, bucket, sizeof(int)* size);
  56. }
  57. }

使用数组方式更为简洁,但是理解上更为麻烦些:

1.我们先试探边界求出最大数的位数。

2.在根据每次个位或者十位等等,这些位上的数据放入一个基数桶中。

3.最后我们要按照0~9这样的顺序插入到临时数组中

4.在插入时我们要根据辅助数组的顺序依次插入,这个类似一维数组表示二维数组的形式。

5.最后拷贝回原数组,释放空间。

**今天到这里就结束啦! **


本文转载自: https://blog.csdn.net/m0_64770095/article/details/125648259
版权归原作者 沙漠下的胡杨 所有, 如有侵权,请联系我们删除。

“排序算法之非比较排序(基数排序和计数排序)”的评论:

还没有评论