当前位置:   article > 正文

【数据结构初阶】十一、归并排序(比较排序)的讲解和实现(递归版本 + 非递归版本 -- C语言实现)

【数据结构初阶】十一、归并排序(比较排序)的讲解和实现(递归版本 + 非递归版本 -- C语言实现)

=========================================================================

相关代码gitee自取

C语言学习日记: 加油努力 (gitee.com)

 =========================================================================

接上期

【数据结构初阶】十、快速排序(比较排序)讲解和实现
(三种递归快排版本 + 非递归快排版本 -- C语言实现)-CSDN博客

 =========================================================================

                     

常见排序算法的实现(续上期)

(详细解释在图片的注释中,代码分文件放下一标题处)
                 

四、归并排序

基本思想:

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

             

归并排序核心步骤:

                      

归并排序的特性总结:
  • 归并的缺点在于需要O(N)的空间复杂度
    归并排序的思考更多的是解决在磁盘中的外排序问题

                      
  • 该算法时间复杂度O(N*logN)
                         
  • 该算法空间复杂度O(N)
                        
  • 该算法稳定性稳定
                           

---------------------------------------------------------------------------------------------

                       

_MergeSort函数
--
归并排序函数(递归版本)的子函数(内部函数), 完成归并操作

  • 之后会涉及递归操作,这里先设置递归结束条件
    当前区间分割成无意义不合理区间返回上层递归
                        
  • 因为要将当前区间分为两部分,所以先获得当前区间的中间下标mid
                      
  • 通过mid对当前区间的左区间进行递归分割出新的左右区间
    同理,当前区间的右区间也进行递归分割出新的左右区间

                    
  • 递归分割完区间后递归返回时再进行归并操作
    分割出的区间元素归并排序后放到动态开辟的tmp数组
    归并完成后拷贝到原数组中
                 
  • 定义当前区间的左右区间范围
    定义一个tmp数组的起始下标index
                   
  • 使用while循环将分割出的区间元素归并到tmp数组中
                     
  • 最后将归并好的tmp数组拷贝回原数组中
图示:

该函数执行逻辑图:

                          

                          
---------------------------------------------------------------------------------------------

                    

MergeSort函数 -- 归并排序函数(递归版本)

  • 开辟动态数组tmp
    连续开辟和待排序数组对应类型和个数的动态空间
                   
  • 调用_MergeSort子函数进行归并操作
                    
  • 执行完归并排序后,释放开辟的动态空间tmp数组
图示:

该函数测试:

                          

                          
---------------------------------------------------------------------------------------------

                    

(难)MergeSortNonR函数 -- 归并排序函数(非递归版本)

  • 开辟动态数组tmp
    连续开辟和待排序数组对应类型和个数的动态空间
                   
  • 因为不能使用递归进行归并前分割操作来分割区间
    所以需要定义一个gap值
                    
  • gap默认为1,来进行一一归二”;
    之后gap*2进行二二归四”;再gap*2进行四四归八”……
    使用while循环循环进行几几归2*几”,
    完成本次while循环后,调整gap值gap*=2*gap),
    进行下次当前gap值几几归2*几操作
                     
  • 内嵌for循环找到要进行归并的两个小区间”,
    确保这两个区间范围不会越界或按需对其进行修正重点),
    然后进行归并操作拷贝操作

                       
  • 退出while循环后,完成非递归归并排序释放掉tmp数组
图示:

该函数测试:

                     

                     


                    

补充:计数排序(非比较排序)

             

本期博客再包含前两期博客中:
                    

【数据结构初阶】九、五种比较排序的讲解和实现
(直接插入 \ 希尔 \ 直接选择 \ 堆 \ 冒泡 -- C语言)-CSDN博客

                          

【数据结构初阶】十、快速排序(比较排序)讲解和实现
(三种递归快排版本 + 非递归快排版本 -- C语言实现)-CSDN博客

                     

我们总共了解了七种比较排序

比较排序需要通过比较元素之间的大小来进行排序的排序算法

                  

先通过下表来总结前面的七种比较排序:
排序算法
(比较排序)
平均情况
(时间复杂度)
最好情况
(时间复杂度)
最坏情况
(时间复杂度)
空间复杂度稳定性
直接插入排序O(N^2)O(N)O(N^2)O(1)稳定
希尔排序O(N*logN) ~ O(N^2)O(N^1.3)O(N^2)O(1)不稳定
直接选择排序O(N^2)O(N^2)O(N^2)O(1)不稳定
堆排序O(N*logN)O(N*logN)O(N*logN)O(1)不稳定
冒泡排序O(N^2)O(N)O(N^2)O(1)稳定
快速排序O(N*logN)O(N*logN)O(N^2)O(logN) ~ O(N)不稳定
归并排序O(N*logN)O(N*logN)O(N*logN)O(N)稳定

                  

再补充了解一种非比较排序计数排序

基本思想:

计数排序又称为鸽巢原理是对哈希直接定址法的变形应用

                   

计数排序核心步骤:
  • 统计相同元素出现的次数
                      
  • 根据统计的结果将序列回收到原来的序列中
                     
归并排序的特性总结:
  • 计数排序数据范围集中时效率很高,但是适用范围及场景有限
                      
  • 该算法时间复杂度O(MAX(N,range))
                         
  • 该算法空间复杂度O(range)
                        

---------------------------------------------------------------------------------------------

                       

CountSort函数 -- 计数排序(鸽巢原理)函数

  • 先使用for循环找到a数组中的最大值max最小值min
                       
  • 通过max和min获得数组a的元素范围range
    开辟等大等数的动态统计数组count检查开辟是否成功
    开辟成功后对其进行初始化
                        
  • 使用for循环在统计数组count中统计对应下标元素出现次数
                        
  • 根据统计的结果序列回收到原来的序列数组
图示:

改函数执行逻辑图:

该函数测试:

         

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

             

对应代码(续上期)

Sort.h -- 排序头文件

  1. //归并排序函数(递归版本):
  2. //第一个参数:接收要排序的数组首元素地址(a)
  3. //第二个参数:接收该数组的长度(n)
  4. void MergeSort(int* a, int n);
  5. //归并排序函数(非递归版本):
  6. //第一个参数:接收要排序的数组首元素地址(a)
  7. //第二个参数:接收该数组的长度(n)
  8. void MergeSortNonR(int* a, int n);
  9. //计数排序(鸽巢原理)-- 非比较排序:
  10. //第一个参数:接收要排序的数组首元素地址(a)
  11. //第二个参数:接收该数组的长度(n)
  12. void CountSort(int* a, int n);

            

            

---------------------------------------------------------------------------------------------

             

Sort.c -- 排序函数实现文件

  1. //归并排序函数(递归版本)的子函数(内部函数):
  2. //第一个参数:接收要排序的数组首元素地址(a)
  3. //第二个参数:接收开辟的等大动态空间首元素地址(tmp)
  4. //第三个参数:接收该数组起始位置下标(0)
  5. //第四个参数:接收该数组尾部位置下标(数组元素个数-1)
  6. //(函数名前加个“_”,表示为该函数的子函数)
  7. void _MergeSort(int* a, int* tmp, int begin, int end)
  8. {
  9. //设置递归结束条件:
  10. if (end <= begin)
  11. //当前区间分割成无意义或不合理区间时:
  12. {
  13. //返回上层递归:
  14. return;
  15. }
  16. //先获得当前区间的中间下标mid:
  17. int mid = (end + begin) / 2;
  18. /*
  19. * 之后就可以将当前区间分成两个区间:
  20. * [begin, mid] [mid+1, end]
  21. * 如果左区间有序,右区间也有序就可以开展归并了
  22. *
  23. * 可以先对当前有效的左区间进行递归,
  24. * 使其不断分割出新的左右区间;
  25. * 同理,也对当前右区间进行递归,不断分割出新的左右区间
  26. * 分割到区间范围: end <= begin 时(不合理、无意义区间),
  27. * 就返回递归不再分割
  28. *
  29. * 递归分割完区间后,递归返回时再进行归并操作
  30. *
  31. * 整个过程类似二叉树的后序遍历,左子树 -> 右子树 -> 根
  32. * 归并:
  33. * 左区间归并后有序 -> 右区间归并后有序 -> 归并排序后结果拷贝到原数组
  34. */
  35. //对当前区间的左区间进行递归,分割出新左右区间:
  36. _MergeSort(a, tmp, begin, mid); //分割后左区间范围:[begin, mid]
  37. //对当前区间的右区间进行递归,分割出新左右区间:
  38. _MergeSort(a, tmp, mid+1, end); //分割后左区间范围:[mid+1, end]
  39. /*
  40. * 递归分割完区间后,递归返回时再进行归并操作,
  41. * 将分割出的区间元素归并到动态开辟的tmp数组中,
  42. * 归并完成后再拷贝到原数组中:
  43. */
  44. //定义当前区间的左区间范围:
  45. int begin1 = begin; //当前左区间的左范围
  46. int end1 = mid; //当前左区间的右范围
  47. //定义当前区间的右区间范围:
  48. int begin2 = mid+1; //当前右区间的左范围
  49. int end2 = end; //当前右区间的右范围
  50. //再定义一个tmp数组的起始下标begin:
  51. int index = begin;
  52. //使用whlie循环将分割出的区间元素归并tmp数组中:
  53. while (begin1 <= end1 && begin2 <= end2)
  54. //当前区间的左右区间范围合理:
  55. {
  56. /*
  57. * 比较当前a数组中当前区间的左右区间元素大小,
  58. * 将a数组当前区间中的较小值尾插放入tmp数组中(循环)
  59. */
  60. if (a[begin1] <= a[begin2])
  61. //如果当前区间的左区间元素小于等于其右区间元素:
  62. {
  63. tmp[index++] = a[begin1++];
  64. //将此时a数组中的当前区间的左区间元素尾插进tmp数组
  65. //放入后++调整位置
  66. }
  67. else
  68. //当前区间的右区间元素小于其左区间元素:
  69. {
  70. tmp[index++] = a[begin2++];
  71. //将此时a数组中的当前区间的右区间元素尾插进tmp数组
  72. //放入后++调整位置
  73. }
  74. }
  75. /*
  76. * 当循环结束时,不知道是左右哪个区间不合理结束了,
  77. * 我们假设循环是左区间合理,右区间不合理而结束,
  78. * 那么此时的左区间就还有元素没尾插到tmp数组中,
  79. * 所以要将其剩下的元素再尾插到tmp数组中(右区间同理):
  80. */
  81. //假设循环是左区间合理,右区间不合理而结束:
  82. while (begin1 <= end1)
  83. //当前左区间还合理(还有元素):
  84. {
  85. //将其剩下的元素尾插进tmp数组中:
  86. tmp[index++] = a[begin1++];
  87. }
  88. //如果循环是右区间合理,左区间不合理而结束:
  89. while (begin2 <= end2)
  90. //当前右区间还合理(还有元素):
  91. {
  92. //将其剩下的元素尾插进tmp数组中:
  93. tmp[index++] = a[begin2++];
  94. }
  95. /*
  96. * 到此就将元素都归并排序到了tmp数组中,
  97. * 最后还有将tmp数组拷贝到原数组中:
  98. */
  99. //使用memcpy库函数将归并好的tmp数组拷贝回原数组:
  100. memcpy(a + begin, tmp + begin, (end - begin + 1)*sizeof(int));
  101. // (目的地) (被拷贝位置) (拷贝数据大小)
  102. // end-begin+1, 即元素个数,因为区间左闭右闭所以还要+1
  103. }
  104. //归并排序函数(递归版本):
  105. //第一个参数:接收要排序的数组首元素地址(a)
  106. //第二个参数:接收该数组的长度(n)
  107. void MergeSort(int* a, int n)
  108. {
  109. //开辟动态数组tmp:
  110. //连续开辟和待排序数组对应类型和个数的动态空间:
  111. int* tmp = (int*)malloc(sizeof(int) * n);
  112. //检查开辟动态空间是否成功:
  113. if (tmp == NULL)
  114. //返回NULL,说明开辟失败:
  115. {
  116. //打印错误信息:
  117. perror("malloc fail");
  118. //返回结束当前函数
  119. return;
  120. }
  121. /*
  122. * 之后要进行归并排序,需要使用递归进行,
  123. * 而上面代码是开辟动态空间,
  124. * 如果在本函数中使用递归进行归并排序操作,
  125. * 不断递归会不断地开辟动态空间,
  126. * 所以接下来的归并排序操作可以交给子函数进行:
  127. */
  128. //调用子函数进行归并排序操作:
  129. _MergeSort(a, tmp, 0, n - 1);
  130. //第一个参数:接收要排序的数组首元素地址(a)
  131. //第二个参数:接收开辟的等大动态空间首元素地址(tmp)
  132. //第三个参数:接收该数组起始位置下标(0)
  133. //第四个参数:接收该数组尾部位置下标(数组元素个数-1)
  134. //(函数名前加个“_”,表示为该函数的子函数)
  135. //执行完归并排序后,释放开辟的动态空间:
  136. free(tmp);
  137. }
  138. //空间复杂度:O(N)
  139. //时间复杂度:O(N*logN)
  140. //归并排序函数(非递归版本):
  141. //第一个参数:接收要排序的数组首元素地址(a)
  142. //第二个参数:接收该数组的长度(n)
  143. void MergeSortNonR(int* a, int n)
  144. {
  145. /*
  146. * 思路:
  147. *
  148. * 递归版本的归并排序是先通过递归将原数组不断分割,
  149. * 分割到无法分割后再进行归并排序操作返回到原数组,
  150. *
  151. * 而非递归版本的归并排序要在一开始就找到原数组的
  152. * 最小区间,对两个最小区间进行归并操作,
  153. * 排序完成一个由两个最小区间组成“大区间”(已有序),
  154. * 最小区间都组成“大区间”后,
  155. * 再对两个“大区间”进行归并操作,组成一个“大大区间”(已有序)
  156. * (以此类推)
  157. * 其中组成“大区间”的“小区间”需要定义一个gap值来控制
  158. *(此过程就是递归版本中开始返回进行归并排序的过程)
  159. *(两区间有序了才能进行归并合成一个有序的大区间)
  160. */
  161. //开辟动态数组tmp:
  162. //连续开辟和待排序数组对应类型和个数的动态空间:
  163. int* tmp = (int*)malloc(sizeof(int) * n);
  164. //检查开辟动态空间是否成功:
  165. if (tmp == NULL)
  166. //返回NULL,说明开辟失败:
  167. {
  168. //打印错误信息:
  169. perror("malloc fail");
  170. //返回结束当前函数
  171. return;
  172. }
  173. //gap = 1时,“一一归二”(11归并):
  174. /*
  175. * “一一归二”:
  176. * 控制一个区间的左右区间都各只有一个元素,
  177. * 再比较左右区间中元素(此时都只有一个元素)大小,
  178. * 进行归并操作将这左右区间归并为一个有两个元素的区间(已排序)
  179. *
  180. * gap = 2时,“二二归四”(22归并)同理;
  181. * gap = 4时,“四四归八”(44归并)同理。
  182. * (gap每次调整都是二倍的关系)
  183. */
  184. int gap = 1; //定义gap值
  185. //最外层while循环控制“几几归”:
  186. while (gap < n)
  187. //当“几”>数组长度时,归并排序完成
  188. {
  189. for (int i = 0; i < n; i += 2 * gap)
  190. /*
  191. * 循环变量修正表达式:i += 2*gap
  192. * 使用该循环变量修正表达式是为了确保
  193. * 除了第一次之后的定义左右区间能够定义正确,
  194. * 左右区间被归并成一个区间后,就不需要再被进行归并了,
  195. * 所以要调整循环变量修正表达式,保证一个区间不会被多次归并
  196. */
  197. {
  198. /*
  199. * gap为1,那么就需要在当前区间中,
  200. * 从左往右依次以2个元素定义一个区间,
  201. * 将该区间再分割为左右两个区间,
  202. * 所以左右区间都各只有1个元素;
  203. *
  204. * gap为2 同理,在当前区间中,
  205. * 从左往右依次以4个元素定义一个区间,
  206. * 将该区间再分割为左右两个区间,
  207. * 所以左右区间都各只有2个元素;
  208. *
  209. * (以此类推)
  210. */
  211. //定义左区间的左范围(下标):
  212. int begin1 = i;
  213. //定义左区间的右范围(下标):
  214. int end1 = i + gap - 1;
  215. //定义右区间的左范围(下标):
  216. int begin2 = i + gap;
  217. //定义右区间的右范围(下标):
  218. int end2 = i + 2 * gap - 1;
  219. //[begin1, end1] [begin2, end2]
  220. //判断以当前gap值划分的“左右区间”是否合理(会不会越界):
  221. if (end1 >= n || begin2 >= n)
  222. /*
  223. * 左右区间:
  224. * [begin1, end1] [begin2, end2]
  225. *
  226. * end1 >= n
  227. * 通过分析,随着gap的增大,begin1不可能会越界,
  228. * 而从end1开始后就有可能会越界了,
  229. * 如果end1都已经越界了的话,那就没必要再进行归并了,
  230. *
  231. * begin2 >= n
  232. * 也有可能“左区间”正常,“右区间”越界了,导致
  233. * “几几归2*几”的第二个“几(右区间)”没了,
  234. * 导致无法将两个“小区间”归并成“大区间”
  235. *
  236. * (如果“当前右区间”左右范围都不合理,
  237. * 这一组就不用进行归并操作了)
  238. */
  239. {
  240. //如果满足其中一种情况就无法进行归并操作了,
  241. //直接break跳出内嵌for循环:
  242. break;
  243. }
  244. if (end2 >= n)
  245. /*
  246. * 如果只有"右区间"的右范围越界(end2 >= n)的话,
  247. * 那么说明“右区间”还有元素可以和“左区间”进行归并,
  248. * 所以要对此时的“右区间”的右范围进行修正:
  249. */
  250. {
  251. end2 = n - 1; //修正到数组的尾元素下标位置
  252. }
  253. //打印查看归并时“几几归2*几”的过程:
  254. printf("当前 “%d%d归%d” 的区间:> ", gap, gap, 2 * gap);
  255. printf("[%d,%d] [%d,%d]\n", begin1, end1, begin2, end2);
  256. /*
  257. * 通过gap分出“想要的左右两个区间(a数组的)”后,
  258. * 对这左右两个区间进行和递归版本相同的归并操作,
  259. * 比较这两个区间后,将“较小区间”尾插进tmp数组,
  260. * 这两个区间在tmp数组中变得有序,将其再拷贝回原数组
  261. */
  262. //再定义一个tmp数组的起始下标begin:
  263. int index = i;
  264. //使用whlie循环将分割出的区间元素归并tmp数组中:
  265. while (begin1 <= end1 && begin2 <= end2)
  266. //当前区间的左右区间范围合理:
  267. {
  268. /*
  269. * 比较当前a数组中当前区间的左右区间元素大小,
  270. * 将a数组当前区间中的较小值尾插放入tmp数组中(循环)
  271. */
  272. if (a[begin1] < a[begin2])
  273. //如果当前区间的左区间元素小于其右区间元素:
  274. {
  275. tmp[index++] = a[begin1++];
  276. //将此时a数组中的当前区间的左区间元素尾插进tmp数组
  277. //放入后++调整位置
  278. }
  279. else
  280. //当前区间的右区间元素小于其左区间元素:
  281. {
  282. tmp[index++] = a[begin2++];
  283. //将此时a数组中的当前区间的右区间元素尾插进tmp数组
  284. //放入后++调整位置
  285. }
  286. }
  287. /*
  288. * 当循环结束时,不知道是左右哪个区间不合理结束了,
  289. * 我们假设循环是左区间合理,右区间不合理而结束,
  290. * 那么此时的左区间就还有元素没尾插到tmp数组中,
  291. * 所以要将其剩下的元素再尾插到tmp数组中(右区间同理):
  292. */
  293. //假设循环是左区间合理,右区间不合理而结束:
  294. while (begin1 <= end1)
  295. //当前左区间还合理(还有元素):
  296. {
  297. //将其剩下的元素尾插进tmp数组中:
  298. tmp[index++] = a[begin1++];
  299. }
  300. //如果循环是右区间合理,左区间不合理而结束:
  301. while (begin2 <= end2)
  302. //当前右区间还合理(还有元素):
  303. {
  304. //将其剩下的元素尾插进tmp数组中:
  305. tmp[index++] = a[begin2++];
  306. }
  307. //使用memcpy库函数将归并好的tmp数组拷贝回原数组:
  308. //(放在内嵌for循环中,归并一组就拷贝一组,没归并就不拷贝了)
  309. memcpy(a + i, tmp + i, (end2-i+1) * sizeof(int));
  310. // (目的地)(被拷贝位置) (拷贝数据大小)
  311. /*
  312. * end2-i+1, 即元素个数,这里“-i”本来应该是begin1,
  313. * 但是begin1在进行归并操作时会被++,所以应该是“-i”,
  314. * 即减去begin1的起始值i,再“+1”是因为“数组尾元素下标+1”
  315. * 才是数组的元素个数
  316. */
  317. } //内嵌for循环
  318. //打印当前一组“几几归2*几”对应的所有区间后,
  319. //进行换行,准备下次“几几归2*几”:
  320. printf("\n");
  321. // “几几归2*几”:
  322. //“一一归二”后,进行“二二归四”,
  323. //“二二归四”后,进行“四四归八”,
  324. //(以此类推,直到“几”>数组长度)
  325. gap *= 2 ;
  326. /*
  327. * 执行到此,以gap=1为准的归并操作就完成了,
  328. * 但是当前只能处理有2^n个元素的数组,
  329. * (如果不对归并的“左右区间”进行限定的话)
  330. * 不然归并会出各种问题,所以要在归并开始前进行判断,
  331. * 看用于归并的“左右区间”会不会越界
  332. */
  333. } //最外层while循环
  334. //执行完归并排序后,释放开辟的动态空间:
  335. free(tmp);
  336. }
  337. //时间复杂度:O(N*logN)
  338. //空间复杂度:O(N)
  339. //(时间和空间复杂度都和递归版本相同)
  340. //计数排序(鸽巢原理)-- 非比较排序:
  341. //第一个参数:接收要排序的数组首元素地址(a)
  342. //第二个参数:接收该数组的长度(n)
  343. void CountSort(int* a, int n)
  344. {
  345. /*
  346. * 思路:
  347. * 1. 统计相同元素出现次数
  348. * 2. 根据统计的结果将序列回收到原来的序列中
  349. * (适合对数据范围相对集中的数组进行排序)
  350. * (只适用于整型)
  351. *
  352. * 简单描述:找出被排序数组a中最大元素(max)和最小元素(min),
  353. * 创建一个以max为尾元素下标的数组count,
  354. * 这样被排序数组a中的元素就都能在count数组的下标中找到,
  355. * 而且count数组的下标是有序的,
  356. * count数组全部下标对应元素一开始都为0,
  357. *
  358. * 1. 统计相同元素出现次数:
  359. * 假设被排序数组a为:[2,1,5,2,4] ,遍历该数组,
  360. * a数组中有元素:2 ,
  361. * 那么就在count数组下标为2的位置元素++ (0变成1)
  362. * 有元素:1,在count数组下标为1的位置元素++ (0变成1)
  363. * 有元素:5,在count数组下标为5的位置元素++ (0变成1)
  364. * 有元素:2,在count数组下标为2的位置元素++ (1变成2)
  365. * 有元素:4,在count数组下标为4的位置元素++ (0变成1)
  366. *
  367. * 2. 根据统计的结果将序列回收到原来的序列中:
  368. * 根据第1步中count数组的统计,count数组为:
  369. *
  370. * 0 1 2 0 1 1
  371. * (下标0)(下标1)(下标2)(下标3)(下标4)(下标5)
  372. *
  373. * 这时遍历count数组,按以下规律覆盖写到原数组a中:
  374. *
  375. * 0有0个,1有1个,2有2个,3有0个,4有1个,5有1个
  376. * (0个的话就不覆盖写了)
  377. * ↓
  378. * a数组:1 2 2 4 5
  379. */
  380. //存储a数组(被排序数组)的最小值:
  381. int min = a[0]; //默认为首元素
  382. //存储a数组(被排序数组)的最大值:
  383. int max = a[0]; //默认为首元素
  384. //使用for循环找到a数组中的最小值和最大值:
  385. for (int i = 0; i < n; i++)
  386. {
  387. //找最小值:
  388. if (a[i] < min)
  389. //当前元素小于min:
  390. {
  391. min = a[i]; //将该元素赋给min
  392. }
  393. //找最大值:
  394. if (a[i] > max)
  395. //当前元素大于max:
  396. {
  397. max = a[i]; //将该元素赋给max
  398. }
  399. }
  400. //定义统计数组count的区间(元素个数):
  401. int range = max - min + 1;
  402. // [min, max] 左闭右闭,所以元素个数还要+1
  403. //为统计数组count开辟动态空间:
  404. int* count = (int*)malloc(sizeof(int) * range);
  405. //检查是否开辟成功:
  406. if (count == NULL)
  407. //开辟后返回空指针:
  408. {
  409. //说明开辟失败,打印错误信息:
  410. perror("malloc fail");
  411. //返回结束函数:
  412. return;
  413. }
  414. //对统计数组进行初始化:
  415. memset(count, 0, sizeof(int) * range);
  416. //使用memset函数将count数组所有元素都初始化为0
  417. // 1. 使用for循环统计相同元素出现次数:
  418. for (int i = 0; i < n; i++)
  419. {
  420. count[a[i] - min]++;
  421. /*
  422. * 这里count数组的下标写成:[a[i] - min]
  423. * 是为了实现 “相对映射” :
  424. * 被排序数组a可能是:[100, 135, 122, 199, 111]
  425. * 这里max为199,传统方式count数组就要开辟 [0, 199]
  426. * 200个空间,而实际只会使用 [100, 199] 这部分空间,
  427. * 所以上面 range = 199 - 100 + 1 = 100 只开辟了100个空间,
  428. * 但是开辟的100个空间,他的下标范围却是 [0, 99]
  429. * a数组中元素无法对应count数组的下标,所以就需要“相对映射”
  430. *
  431. * 相对映射:
  432. * a数组元素 - 节省开辟的空间数(这里是100) = 相对下标
  433. * 以元素100为例: 100 - 100 = 0 ,
  434. * 这时 a数组中的元素100 就对应 count数组中下标0,
  435. * a数组中每有一个元素100,count数组下标0元素++,
  436. * a数组中其它元素同理。到时进行恢复操作的时候再将
  437. * count下标 + 节省开辟的空间数(这里是100) 即可
  438. * (即 a数组中对应元素)
  439. */
  440. }
  441. // 2. 根据统计的结果将序列回收到原来的序列(数组)中:
  442. int j = 0; //用于遍历a数组(原数组)下标
  443. for (int i = 0; i < range; i++)
  444. //range为数组元素所在范围(区间)
  445. {
  446. while (count[i]-- != 0)
  447. /*
  448. * count[i] 为统计数组的当前下标i元素,
  449. * 元素为几,说明该元素下标i在a数组中所对应的元素有几个,
  450. * 如果count数组中i下标元素为0,则不考虑该下标,
  451. * 这里是将i下标元素不为0的进行操作,
  452. * 将其i下标在a数组中对应的元素一一恢复:
  453. */
  454. {
  455. a[j++] = i + min;
  456. /*
  457. * 因为我们使用了 "相对映射"
  458. * 所以 i下标 + 节省开辟的空间数 == a数组中对应元素
  459. */
  460. }
  461. }
  462. }
  463. //时间复杂度:O(N + range)
  464. /*
  465. * 如果排序的数据比较集中,那么range会比较小 -- O(N)
  466. * 如果不集中,range会比较大 -- O(range)
  467. */
  468. //空间复杂度:O(range)

            

            

---------------------------------------------------------------------------------------------

             

Test.c -- 排序测试文件

  1. //归并排序(递归版本)测试:
  2. void MSTest()
  3. {
  4. //创建要进行插入排序的数组:
  5. int a[] = { 9,1,2,5,7,4,8,6,3,5 };
  6. //调用快速排序(递归版本)进行排序:
  7. MergeSort(a, (sizeof(a) / sizeof(int)));
  8. //使用自定义打印函数打印排序后数组:
  9. printf("使用归并排序(递归版本)后的数组:> ");
  10. PrintArray(a, sizeof(a) / sizeof(int));
  11. }
  12. //归并排序(非递归版本)测试:
  13. void MSNRTest()
  14. {
  15. //创建要进行插入排序的数组:
  16. int a[] = { 9,1,2,5,7,4,8,6,3,5 };
  17. //调用快速排序(非递归版本)进行排序:
  18. MergeSortNonR(a, (sizeof(a) / sizeof(int)));
  19. //使用自定义打印函数打印排序后数组:
  20. printf("使用归并排序(非递归版本)后的数组:> ");
  21. PrintArray(a, sizeof(a) / sizeof(int));
  22. }
  23. //计数排序(鸽巢原理)测试:
  24. void CSTest()
  25. {
  26. //创建要进行插入排序的数组:
  27. int a[] = { 100, 135, 122, 199, 111 };
  28. //调用计数排序(鸽巢原理)进行排序:
  29. CountSort(a, (sizeof(a) / sizeof(int)));
  30. //使用自定义打印函数打印排序后数组:
  31. printf("使用计数排序(鸽巢原理)后的数组:> ");
  32. PrintArray(a, sizeof(a) / sizeof(int));
  33. }
  34. int main()
  35. {
  36. //ISTest();
  37. //SSTest();
  38. //BSTest();
  39. //SlSTest();
  40. //QSTest();
  41. //QSNRTest();
  42. //MSTest();
  43. //MSNRTest();
  44. CSTest();
  45. return 0;
  46. }
本文内容由网友自发贡献,转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号