一.堆排序的优缺点(pros and cons)

(还是简单的说说这个,毕竟没有必要浪费时间去理解一个糟糕的的算法)

优点:

  1. 堆排序的效率与快排、归并相同,都达到了基于比较的排序算法效率的峰值(时间复杂度为O(nlogn))
  2. 除了高效之外,最大的亮点就是只需要O(1)的辅助空间了,既最高效率又最节省空间,只此一家了
  3. 堆排序效率相对稳定,不像快排在最坏情况下时间复杂度会变成O(n^2)),所以无论待排序序列是否有序,堆排序的效率都是O(nlogn)不变(注意这里的稳定特指平均时间复杂度=最坏时间复杂度,不是那个“稳定”,因为堆排序本身是不稳定的)

缺点:(从上面看,堆排序几乎是完美的,那么为什么最常用的内部排序算法是快排而不是堆排序呢?)

  1. 最大的也是唯一的缺点就是——堆的维护问题,实际场景中的数据是频繁发生变动的,而对于待排序序列的每次更新(增,删,改),我们都要重新做一遍堆的维护,以保证其特性,这在大多数情况下都是没有必要的。(所以快排成为了实际应用中的老大,而堆排序只能在算法书里面顶着光环,当然这么说有些过分了,当数据更新不很频繁的时候,当然堆排序更好些...)

二.内部原理

首先要知道堆排序的步骤:

  1. 构造初始堆,即根据待排序序列构造第一个大根堆或者小根堆(大根堆小根堆是什么?这个不解释了,稻草垛知道吧..)
  2. 首尾交换,断尾重构,即对断尾后剩余部分重新构造大(小)根堆
  3. 重复第二步,直到首尾重叠,排序完成

按小根堆排序结果是降序(或者说是非升序,不要在意这种细节..),按大根堆排序的结果是升序

上面这句话乍看好像不对(小根堆中最小元素在堆顶,数组组堆顶元素就是a[0],怎么会是降序?),不过不用质疑这句话的正确性,看了下面这几幅图就明白了:

假设待排序序列是a[] = {7, 1, 6, 5, 3, 2, 4},并且按大根堆方式完成排序

  • 第一步(构造初始堆):

{7, 5, 6, 1, 3, 2, 4}已经满足了大根堆,第一步完成

  • 第二步(首尾交换,断尾重构):

  • 第三步(重复第二步,直至所有尾巴都断下来):

无图,眼睛画瞎了,mspaint实在不好用。。到第二步应该差不多了吧,剩下的用笔也就画出来了。。

其实核心就是“断尾”,但可悲的是所有的资料上都没有明确说出来,可是,还有比“断尾”更贴切的描述吗?

三.实现细节

原理介绍中给出的图基本上也说清楚了实现细节,所以这里只关注代码实现

  • 首先是自己写出来的大根堆方式实现:
  1. #include<stdio.h>
  2.  
  3. //构造大根堆(让a[m]到a[n]满足大根堆)
  4. void HeapAdjust(int a[], int m, int n){
  5. int temp;
  6. int max;
  7. int lc;//左孩子
  8. int rc;//右孩子
  9.  
  10. while(1){
  11. //获取a[m]的左右孩子
  12. lc = 2 * m + 1;
  13. rc = 2 * m + 2;
  14. //比较a[m]的左右孩子,max记录较大者的下标
  15. if(lc >= n){
  16. break;//不存在左孩子则跳出
  17. }
  18. if(rc >= n){
  19. max = lc;//不存在右孩子则最大孩子为左孩子
  20. }
  21. else{
  22. max = a[lc] > a[rc] ? lc : rc;//左右孩子都存在则找出最大孩子的下标
  23. }
  24. //判断并调整(交换)
  25. if(a[m] >= a[max]){//父亲比左右孩子都大,不需要调整,直接跳出
  26. break;
  27. }
  28. else{//否则把小父亲往下换
  29. temp = a[m];
  30. a[m] = a[max];
  31. a[max] = temp;
  32. //准备下一次循环,注意力移动到孩子身上,因为交换之后以孩子为根的子树可能不满足大根堆
  33. m = max;
  34. }
  35. }
  36. }
  37.  
  38. void HeapSort(int a[], int n){
  39. int i,j;
  40. int temp;
  41.  
  42. //自下而上构造小根堆(初始堆)
  43. for(i = n / 2 - 1;i >= 0;i--){//a[n/2 - 1]恰好是最后一个非叶子节点(叶子节点已经满足小根堆,只需要调整所有的非叶子节点),一点小小的优化
  44. HeapAdjust(a, i, n);
  45. }
  46.  
  47. printf("初始堆: ");
  48. for(i = 0;i < n;i++){
  49. printf("%d ", a[i]);
  50. }
  51. printf("\n");
  52.  
  53. for(i = n - 1;i > 0;i--){
  54. //首尾交换,断掉尾巴
  55. temp = a[i];
  56. a[i] = a[0];
  57. a[0] = temp;
  58. //断尾后的部分重新调整
  59. HeapAdjust(a, 0, i);
  60.  
  61. /*
  62. printf("第%d次(i - 1 = %d): ", n - i, i - 1);
  63. for(j = 0;j < n;j++){
  64. printf("%d ", a[j]);
  65. }
  66. printf("\n");
  67. */
  68. }
  69. }
  70.  
  71. main(){
  72. //int a[] = {5, 6, 3, 4, 1, 2, 7};
  73. //int a[] = {1, 2, 3, 4, 5, 6, 7};
  74. //int a[] = {7, 6, 5, 4, 3, 2, 1};
  75. int a[] = {7, 1, 6, 5, 3, 2, 4};
  76. int m, n;
  77. int i;
  78.  
  79. m = 0;
  80. n = sizeof(a) / sizeof(int);
  81. //HeapAdjust(a, m, n);
  82. HeapSort(a, n);
  83. printf("结果: ");
  84. for(i = 0;i < n;i++){
  85. printf("%d ", a[i]);
  86. }
  87. printf("\n");
  88. }

P.S.代码中注释极其详尽,因为是完全一步一步自己想着写出来的,应该不难理解。看代码说话,在此多说无益。

  • 接下来给出书本上的大根堆方式实现:
  1. #include<stdio.h>
  2.  
  3. void HeapAdjust(int a[], int m, int n){
  4. int i;
  5. int t = a[m];
  6.  
  7. for(i = 2 * m + 1;i <= n;i = 2 * i + 1){
  8. if(i < n && a[i + 1] > a[i])++i;
  9. if(t >= a[i])break;
  10. //把空缺位置往下放
  11. a[m] = a[i];
  12. m = i;
  13. }
  14. a[m] = t;//只做一次交换,步骤上的优化
  15. }
  16.  
  17. void HeapSort(int a[], int n){
  18. int i;
  19. int t;
  20.  
  21. //自下而上构造大根堆
  22. for(i = n / 2 - 1;i >= 0;--i){
  23. HeapAdjust(a, i, n - 1);
  24. }
  25.  
  26. printf("初始堆: ");
  27. for(i = 0;i < n;i++){
  28. printf("%d ", a[i]);
  29. }
  30. printf("\n");
  31.  
  32. for(i = n - 1;i > 0;i--){
  33. //首尾交换,断掉尾巴
  34. t = a[i];
  35. a[i] = a[0];
  36. a[0] = t;
  37. //对断尾后的部分重新建堆
  38. HeapAdjust(a, 0, i - 1);
  39. }
  40. }
  41.  
  42. main(){
  43. //int a[] = {5, 6, 3, 4, 1, 2, 7};
  44. //int a[] = {1, 2, 3, 4, 5, 6, 7};
  45. //int a[] = {7, 6, 5, 4, 3, 2, 1};
  46. int a[] = {7, 1, 6, 5, 3, 2, 4};
  47. int m, n;
  48. int i;
  49.  
  50. m = 0;
  51. n = sizeof(a) / sizeof(int);
  52. //HeapAdjust(a, m, n);
  53. HeapSort(a, n);
  54. printf("结果: ");
  55. for(i = 0;i < n;i++){
  56. printf("%d ", a[i]);
  57. }
  58. printf("\n");
  59. }

P.S.书本上的代码短了不少,不仅仅是篇幅上的优化,也有实实在在的步骤上的优化,细微差别也在注释中说明了。但这种程度的优化却使得代码的可读性大大降低,所以一次次拿起算法书,又一次次放下。。(实际应用中我们可以对书本上的代码做形式上的优化,在保持其高效性的同时尽可能的提升其可读性。。)

  • 最后是在研究过书本上的算法之后,结合其优化措施,写出的小根堆方式实现(网上的资料多是大根堆方式的,其实原理都一样,这里只是为了避免枯燥无趣。。):
  1. #include<stdio.h>
  2.  
  3. //构造小根堆(让a[m]到a[n]满足小根堆)
  4. void HeapAdjust(int a[], int m, int n){
  5. int i;
  6. int t = a[m];
  7. int temp;
  8.  
  9. for(i = 2 * m + 1;i <= n;i = 2 * i + 1){
  10. //a[m]的左右孩子比较,i记录较小者的下标
  11. if(i < n && a[i + 1] < a[i]){
  12. i = i + 1;
  13. }
  14. if(t <= a[i]){
  15. break;
  16. }
  17. else{//把空缺位置往下换
  18. //把较小者换上去
  19. temp = a[m];
  20. a[m] = a[i];
  21. a[i] = temp;
  22. //准备下一次循环
  23. m = i;
  24. }
  25. }
  26. }
  27.  
  28. void HeapSort(int a[], int n){
  29. int i, j;
  30. int temp;
  31.  
  32. //自下而上构造小根堆(初始堆)
  33. for(i = n / 2 - 1;i >= 0;i--){//a[n/2 - 1]恰好是最后一个非叶子节点(叶子节点已经满足小根堆,只需要调整所有的非叶子节点),一点小小的优化
  34. HeapAdjust(a, i, n);
  35. }
  36.  
  37. printf("初始堆: ");
  38. for(i = 0;i < n;i++){
  39. printf("%d ", a[i]);
  40. }
  41. printf("\n");
  42.  
  43. //把每个元素都调整到应该去的位置
  44. for(i = n - 1; i > 0;i--){
  45. //首尾交换
  46. temp = a[i];
  47. a[i] = a[0];
  48. a[0] = temp;
  49. //断尾后剩余部分重新调整
  50. HeapAdjust(a, 0, i - 1);
  51. }
  52. }
  53.  
  54. main(){
  55. //int a[] = {7, 6, 5, 4, 3, 2, 1};
  56. //int a[] = {1, 5, 6, 4, 3, 2, 7};
  57. int a[] = {1, 2, 3, 4, 5, 6, 7};
  58. int m, n;
  59. int i;
  60.  
  61. m = 0;
  62. n = sizeof(a) / sizeof(int);
  63. //HeapAdjust(a, m, n);
  64. HeapSort(a, n);
  65. printf("结果: ");
  66. for(i = 0;i < n;i++){
  67. printf("%d ", a[i]);
  68. }
  69. printf("\n");
  70. }

P.S.注释依然详尽,看代码,不废话

四.总结

堆排序的步骤就几个字而已:建堆 -> 首尾交换,断尾重构 -> 重复第二步,直到断掉所有尾巴

还有比这更清晰更明了的描述吗?

到现在我们已经掌握了几个有用的排序算法了:

快速排序归并排序、堆排序

那么实际应用中要如何选择呢?有这些选择标准:

  1. 若n较小,采用插入排序和简单选择排序。由于直接插入排序所需的记录移动操作比简单选择排序多,所以当记录本身信息量比较大时,用简单选择排序更好。
  2. 若待排序序列基本有序,可以采用直接插入排序或者冒泡排序
  3. 若n较大,应该采用时间复杂度最低的算法,比如快排,堆排或者归并
    • 细分的话,当数据随机分布时,快排最佳(这与快排的硬件优化有关,在之前的博文中有提到过)
    • 堆排只需要一个辅助空间,而且不会出现快排的最坏情况
    • 快排和堆排都是不稳定的,如果要求稳定的话可以采用归并,还可以把直接插入排序和归并结合起来,先用直接插入获得有序碎片,再归并,这样得到的结果也是稳定的,因为直接插入是稳定的

说明:在理解“断尾”的过程中参考了前辈的博文,特此感谢

排序算法之堆排序(Heapsort)解析的更多相关文章

  1. 排序算法FOUR:堆排序HeapSort

    /** *堆排序思路:O(nlogn) * 用最大堆,传入一个数组,先用数组建堆,维护堆的性质 * 再把第一个数与堆最后一个数调换,因为第一个数是最大的 * 把堆的大小减小一 * 再 在堆的大小上维护 ...

  2. Java常见排序算法之堆排序

    在学习算法的过程中,我们难免会接触很多和排序相关的算法.总而言之,对于任何编程人员来说,基本的排序算法是必须要掌握的. 从今天开始,我们将要进行基本的排序算法的讲解.Are you ready?Let ...

  3. Java排序算法之堆排序

    堆的概念: 堆是一种完全二叉树,非叶子结点 i 要满足key[i]>key[i+1]&&key[i]>key[i+2](最大堆) 或者 key[i]<key[i+1] ...

  4. 《排序算法》——堆排序(大顶堆,小顶堆,Java)

    十大算法之堆排序: 堆的定义例如以下: n个元素的序列{k0,k1,...,ki,-,k(n-1)}当且仅当满足下关系时,称之为堆. " ki<=k2i,ki<=k2i+1;或k ...

  5. C++编程练习(13)----“排序算法 之 堆排序“

    堆排序 堆是具有下列性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆(也叫最大堆):或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆(也叫最小堆). 最小堆和最大堆如 ...

  6. 数据结构与算法之PHP排序算法(堆排序)

    一.堆的定义 堆通常是一个可以被看做一棵树的数组对象,其任一非叶节点满足以下性质: 1)堆中某个节点的值总是不大于或不小于其父节点的值: 每个节点的值都大于或等于其左右子节点的值,称为大顶堆.即:ar ...

  7. 八大排序算法之七—堆排序(Heap Sort)

    堆排序是一种树形选择排序,是对直接选择排序的有效改进. 基本思想: 堆的定义如下:具有n个元素的序列(k1,k2,...,kn),当且仅当满足 时称之为堆.由堆的定义可以看出,堆顶元素(即第一个元素) ...

  8. 排序算法(2) 堆排序 C++实现

    堆 1 数组对象 2 可以视为一棵完全二叉树 3 一个堆可以被看作一棵二叉树和一个数组,如下图所示: 4 下标计算(通常使用内联函数或者宏来定义下标操作): 已知某个结点的下标为i 其父节点下标:i/ ...

  9. Python 一网打尽<排序算法>之堆排序算法中的树

    本文从树数据结构说到二叉堆数据结构,再使用二叉堆的有序性对无序数列排序. 1. 树 树是最基本的数据结构,可以用树映射现实世界中一对多的群体关系.如公司的组织结构.网页中标签之间的关系.操作系统中文件 ...

随机推荐

  1. scrollLeft滚动(用animate替代)

    原: let checkedLeft1 = $('#dateBox').find('.checked').position().left let checkedLeft2 = $('#dateBox' ...

  2. 第八章 高级搜索树 (b3)B-树:查找

  3. Spring Boot中使用Websocket搭建即时聊天系统

    1.首先在pom文件中引入Webscoekt的依赖 <!-- websocket依赖 --> <dependency> <groupId>org.springfra ...

  4. 安装Python3后,centos使用yum报错

    题记 在之前的文章中我自定义安装了Python3,并且修改了默认的 Python软链,今天想搭建一个 ftp 服务器,使用命令的时候出现了一个错误: 问题 1.使用 yum 安装 ftp工具 yum ...

  5. 121. Best Time to Buy and Sell Stock买卖股票12

    一 [抄题]: If you were only permitted to complete at most one transaction (ie, buy one and sell one sha ...

  6. swift UICollectionView使用

    方法1:创建 的时候注册 layout /// 时间view private lazy var timeCollectionV: UICollectionView = { 1.直接注册 并设置好 UI ...

  7. Volley的使用

    Volley加载图片到控件上 VolleyUtils.getLoader(getContext()).get(zixun.getPicurl(), ImageLoader.getImageListen ...

  8. catkin-tools

    http://catkin-tools.readthedocs.io/en/latest/cheat_sheet.html 一.Initializing Workspaces初始化工作空间 初始化具有 ...

  9. Maven 学习笔记(一) 基础环境搭建

    在Java的世界里,项目的管理与构建,有两大常用工具,一个是Maven,另一个是Gradle,当然,还有一个正在淡出的Ant.Maven 和 Gradle 都是非常出色的工具,排除个人喜好,用哪个工具 ...

  10. 使用jdbc编程实现对数据库的操作以及jdbc问题总结

    1.创建数据库名为mybatis. 2. 在数据库中建立两张表,user与orders表: (1)user表: (2)orders表: 3.创建工程 * 开发环境: * eclipse mars *  ...