当前位置:   article > 正文

通俗易懂的讲解堆排序(含Gif图)_堆排序删除算法动图

堆排序删除算法动图

堆的定义

堆排序是一种树形结构选择排序方法,其特点是:在排序过程中,将序列视为一颗完全二叉树的顺序存储结构,利用完全二叉树中双亲结点和孩子结点之间的关系,在当前无序区间中选择关键字最大(或最小)的元素。堆的定义如下:

n 个关键字序列 L[1,2,3...n] 称为堆,当且仅当该序列满足:

① L(i) <= L(2i) 且 L(i) <= L(2i+1)         ② L(i) >= L(2i) 且 L(i) >= L(2i+1)                   (1 <= i <= n/2)

满足第一种情况的堆称为小根堆(或小顶堆),满足第二种情况的堆称为大根堆(大顶堆)。在大根堆中,最大的元素存放在根结点中。对其任一非根结点,它的值小于等于其父亲结点的值。小根堆的定义刚好相反,根结点是最小元素。建立一个堆有两种方法,很多书籍把这两种方法称为向上调整和向下调整:

向下调整是让调整的结点与其孩子结点进行比较
向上调整是让调整的结点与其父亲结点进行比较

 

向上调整

向上调整需要从根结点开始建堆(以小根堆为例),基本思路:每添加一个元素,将该元素与其父结点进行比较,若新增结点小于父结点,则交换两个结点的值,然后继续与上一级的父结点进行比较。停止向上调整的条件是该结点大于父结点的值。现有序列 list [ ] = {87, 45, 78, 32, 17, 65, 53, 9} 分别用向上调整和向下调整进行建堆,看看这两种方法有什么区别。

向上调整建堆的过程:

添加元素 9 的向上调整 gif:

向上调整代码:

  1. void AdjustUp(int a[],int size)
  2. { //size是数组目前的大小
  3. int temp,flag=0;
  4. if(size==1)
  5. return; //此时是堆顶,不需要上调
  6. while(size!=1 && flag==0)
  7. {
  8. if(a[size] < a[size/2]) //与父结点进行对比
  9. {
  10. temp = a[size];
  11. a[size] = a[size/2];
  12. a[size/2] = temp;
  13. }
  14. else
  15. {
  16. flag = 1;
  17. }
  18. size = size/2; //更新编号i为它父结点的编号。
  19. }
  20. }
  21. void AdjustUp1(int a[], int k)
  22. {//参数k为向上调整的结点,也是堆的元素个数
  23. if(k == 1)
  24. return;
  25. a[0] = a[k];
  26. int i = k/2;
  27. while(i > 0 && a[i] > a[0]) //若结点值小于父结点,则将父结点向下调
  28. {
  29. a[k] = a[i]; //父节点向下调
  30. k = i;
  31. i = k/2; //继续向上比较
  32. }
  33. a[k] = a[0];
  34. }

 

向下调整

向下调整建堆是一个反复筛查的过程。n 个结点的完全二叉树,最后一个结点是第⎿n/2个结点的孩子(完全二叉树的性质)。对第n/2个结点为根的子树筛查(对于小根堆,若结点的关键字大于左右孩子中关键字较小者,则交换),使该子树成为堆。之后向前依次对各结点(n/2-1 ~ 1)为根的子树进行筛选,看该结点值是否小于其左右子结点的值,若不小于,则将左右子结点中的较小值与之交换,交换后可能会破坏下一级的堆,于是继续采取上述方法构造下一级的堆,直到以该结点为根的子树构成堆为止。反复利用上述调整方法建堆,直到根结点。根据原始数据建立的树形结构如下:

根据向下调整的思路,从第 4 个结点开始进行筛查建立小根堆:

向下调调整思路:将该结点与其孩子结点进行比较,选取其中最小进行交换,若该节点本身就是最小的结点,则不需要进行交换操作。如上图,需要将 9 和 32 进行交换。然后对第 3 个结点进行调整,该过程较为简单,所以省略。接下来展示第 2 个结点的调整情况。

对第 2 个结点进行调整时,需要交换 9 和 45。但是交换后发现 32 比 45 要小,所以还需要进行一次交换操作。该过程告诉我们,在交换父子结点后,子结点的堆属性可能被破坏,所以要对子结点进行调整(直至子结点为叶子结点为止)。以下是第 2 个结点最终的调整情况:

向下调整的最终形态:

向下调整 gif:

向下调整代码:

  1. void heap_ajust_min(int *b, int i, int size) //a为数组,size为堆的大小
  2. {
  3. int lchild = 2*i; //i的左孩子节点序号
  4. int rchild = 2*i +1; //i的右孩子节点序号
  5. int min = i; //记录根和左右子树中最小的数的下标
  6. int temp;
  7. if(i <= size/2) //调整不需要从叶结点开始
  8. {
  9. if(lchild<=size && b[lchild]<b[min])
  10. {
  11. min = lchild;
  12. }
  13. if(rchild<=size && b[rchild]<b[min])
  14. {
  15. min = rchild;
  16. } //两个if语句寻找三个结点中最小的数
  17. if(min != i) //如果min不等于i,说明最小的值在左右子树中
  18. {
  19. temp = b[i]; //交换a[i]和a[min]的值
  20. b[i] = b[min];
  21. b[min] = temp;
  22. heap_ajust_min(b, min, size); //被交换的子树可能不满足堆的定义,需要对被交换的子树重新调整
  23. }
  24. }
  25. }
  26. void build_heap_min(int *b, int size) //建立小根堆
  27. {
  28. int i;
  29. for(i = size/2; i >= 1; i--) //非叶子节点最大序号值为size/2,从这个结点开始调整
  30. { //注意for中的循环条件(i = size/2; i >= 1; i--)
  31. heap_ajust_min(b, i, size);
  32. }
  33. }

向下调整的时间与树的高度有关,为 O(h)。建堆过程中每次向下调整时,大部分结点的高度都比较小。我们很容易会发现向上调整和向下调整的最终结果有所不同,但都满足了小根堆的性质。向上调整和向下调整都能够完成建立堆的工作。区别在于,使用向上调整建堆需要从第一个元素开始,过程相当于每次插入一个元素,然后再进行向上调整的工作。向下调整充分的利用了完全二叉树的性质,从n/2⏌结点到根结点逐一筛查。它们的具体操作如下:

  1. for(k = 1; k < size ; ++k) //向上调整建堆
  2. {
  3. AdjustUp(b, k);
  4. }
  5. for(i = size/2; i >= 1; i--) //向下调整建堆
  6. {
  7. heap_ajust_min(b, i, size);
  8. }

 

堆排序

应用堆进行排序的思路很简单:首先将存放在 L[1,2,3...n] 中的 n 个元素建成初始堆,由于堆本身的特点,堆顶元素就是最值。输出堆顶元素后,通常将堆底元素送入堆顶,此时根结点不满足堆的性质,经过向下调整后使其恢复堆的性质,再输出堆顶元素。重复操作,直至最后一个元素为止。

堆排序代码:

  1. void heap_sort_min(int *a, int size)
  2. {
  3. int i;
  4. int temp;
  5. for(i = size; i >= 1; i--)
  6. {
  7. temp = a[1];
  8. a[1] = a[i];
  9. a[i] = temp; //交换堆顶和最后一个元素
  10. heap_ajust_min(a, 1, i-1); //再一次调整堆顶节点成为小顶堆
  11. }
  12. }

利用上述小根堆代码完成排序,会发现序列是倒序的,这是因为排序过程是从后往前不断调整小根堆的结果。如果想要获得升序序列,需要使用大根堆进行排序。

堆支持删除和插入操作,删除元素时,需要将最后一个元素放到堆顶,然后使用向下调整,使堆保持堆的特性。对堆进行插入操作时,先将新结点放在堆的末端,再对这个新结点进行向上调整操作。注意:插入元素时(小根堆为例),只需要调用一次向上调整函数便能恢复堆的属性。因为在插入之前,堆本身是完整的。此时若插入元素小于父结点的值,那么只需要交换两个结点而不需要考虑别的情况。这是因为之前满足堆性质,而插入了一个更小的值,交换后堆属性不变。具体可以看添加 9 的时候的 gif。可以根据需求,将向上调整和向下调整搭配着使用。例如:小根堆删除元素后使用向下调整恢复堆的性质。

完整代码:

  1. #include<stdio.h>
  2. #include<math.h>
  3. void heap_ajust_min(int *b, int i, int size) /*a为堆存储数组,size为堆的大小*/
  4. {
  5. int lchild = 2*i; //i的左孩子节点序号
  6. int rchild = 2*i +1; //i的右孩子节点序号
  7. int min = i; /*存放三个顶点中最大的数的下标*/
  8. int temp;
  9. if(i <= size/2) //假设i是叶节点就不用进行调整
  10. {
  11. if(lchild<=size && b[lchild]<b[min])
  12. {
  13. min = lchild;
  14. }
  15. if(rchild<=size && b[rchild]<b[min])
  16. {
  17. min = rchild;
  18. }
  19. if(min != i)
  20. {
  21. temp = b[i]; /*交换a[i]和a[max]的值*/
  22. b[i] = b[min];
  23. b[min] = temp;
  24. heap_ajust_min(b, min, size); /*被交换的位置曾经是大根堆,如今可能不是大根堆
  25. 所以须要又一次调整使其成为大根堆结构*/
  26. }
  27. }
  28. }
  29. void build_bheap_min(int *b, int size) //建立小堆
  30. {
  31. int i;
  32. for(i=size/2; i >= 1; i--) //非叶节点最大序号值为size/2
  33. {
  34. heap_ajust_min(b, i, size);
  35. }
  36. }
  37. void heap_sort_min(int *a, int size)
  38. {
  39. int i;
  40. int temp;
  41. for(i = size; i >= 1; i--)
  42. {
  43. temp = a[1];
  44. a[1] = a[i];
  45. a[i] = temp; //交换堆顶和最后一个元素
  46. heap_ajust_min(a, 1, i-1); //再一次调整堆顶节点成为小顶堆
  47. }
  48. }
  49. void AdjustUp(int a[],int size)
  50. {
  51. int temp,flag=0;
  52. int k;
  53. if(size==1)
  54. return;//此时是堆顶,不需要上调
  55. while(size!=1 && flag==0)
  56. {
  57. if(a[size] < a[size/2])
  58. {
  59. temp = a[size];
  60. a[size] = a[size/2];
  61. a[size/2] = temp;
  62. }
  63. else
  64. {
  65. flag = 1;
  66. }
  67. size = size/2;//更新编号i为它父结点的编号。
  68. }
  69. }
  70. void AdjustUp1(int a[], int k)
  71. {//参数k为向上调整的结点,也是堆的元素个数
  72. if(k == 1)
  73. return;
  74. a[0] = a[k];
  75. int i = k/2;
  76. while(i > 0 && a[i] > a[0]) //若结点值小于父结点,则将父结点向下调
  77. {
  78. a[k] = a[i]; //父节点向下调
  79. k = i;
  80. i = k/2; //继续向上比较
  81. }
  82. a[k] = a[0];
  83. }
  84. int main(int argc, char *argv[])
  85. {
  86. int i,j,k;
  87. int count=1;
  88. int a[]={0, 87, 45, 78, 32, 17, 65, 53, 9};
  89. int b[]={0, 87, 45, 78, 32, 17, 65, 53, 9};
  90. int size = sizeof(a)/sizeof(int) -1;
  91. build_bheap_min(a, size);
  92. printf("向下调整建立小顶堆:\n");
  93. count=1;
  94. for(i=0;i<=4;i++)
  95. {
  96. for(j=0;j<pow(2,i);j++)
  97. {
  98. if(count<=size)
  99. {
  100. printf("%d ",a[count++]);
  101. }else{
  102. break;
  103. }
  104. }
  105. printf("\n");
  106. }
  107. for(k = 1; k < 9 ; ++k)
  108. {
  109. AdjustUp(b, k);
  110. }
  111. printf("向上调整小顶堆:\n");
  112. count=1;
  113. for(i=0;i<=4;i++)
  114. {
  115. for(j=0;j<pow(2,i);j++)
  116. {
  117. if(count<=size)
  118. {
  119. printf("%d ",b[count++]);
  120. }else{
  121. break;
  122. }
  123. }
  124. printf("\n");
  125. }
  126. return 0;
  127. }

 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/神奇cpp/article/detail/835262
推荐阅读
相关标签
  

闽ICP备14008679号