前言:在学习libevent代码时,看到一个关于时间最小堆的用法,于是想填一下堆排序算法这个坑。花费了大概两天时间才算是搞清楚了,于是有了这篇博文,以此记录。
首先,什么是堆排序算法呢?
顾名思义,就是基于堆的排序算法^^。那么堆排序算法就分为两步:第一步是建立一个堆,第二步就是排序。
堆有大小堆之分,区别就在于大堆的堆顶的元素是所有元素中最大的,而小堆堆顶的元素是所有元素最小的。
在《数据结构 C语言版》(严蔚敏)的书,关于堆排序算法是这么说的:若在输出堆顶的最小值之后,使得剩余n-1元素的序列重新又建成一个堆,则得到n个元素中的次小值,如此反复执行,便能得到一个有序序列,这个过程称为堆排序。由此,实现堆排序需要解决两个问题:(1)如何由以一个无序序列组建成一个堆?(2)如何在输出堆顶元素之后,调整剩余元素成为一个新的堆?
一、关于大小堆以及如何建堆
1. 关于大小堆
堆是一个完全二叉树,什么叫完全二叉树呢?顺便说一下几种二叉树的区别,如下:
完满(full)二叉树(又叫做满二叉树),完全(complete)二叉树和完美(perfect)二叉树:
完满(full)二叉树的特性:只要你有孩子,就一定有两个孩子
完美(perfect)二叉树的特性:深度为k(>=-1)且有2^(k+1) - 1个结点的二叉树
完全(complete)二叉树的特性:从根结点到倒数第二层满足完美二叉树,最后一层可以不完全填充,其叶子结点都靠左对齐。
如果使用数组,那么下标从0开始,父节点是i,则左子树是2*i+1,右子树是2*i+2。如果子节点是i,则父节点是(i-1)/2。n个关键字序列array[0,...,n-1],当且仅当满足下列要求:(0 <= i <= (n-2)/2)
① array[i] <= array[2*i + 1] 且 array[i] <= array[2*i + 2]; 称为小根堆;
② array[i] >= array[2*i + 1] 且 array[i] >= array[2*i + 2]; 称为大根堆;
说明:这里要注意i的取值上限,(n-2)/2是堆中最后一个非终端结点,什么意思呢?就是说,(n-2)/2这个结点一定有左子树(不一定有右子树,为什么?那是因为完全二叉树的特性),进一步可以这么说,(n-2)/2这个节点的孩子一定是最后一个节点n-1(也可以说最后一个节点n-1的父结点是(n-2)/2,其有可能是左子树或者右子树。)
(注意,在数据结构 C语言版》(严蔚敏)的书中,下标是从1开始,假如父结点是i,那么左子树为2*i,右子树为2*i+1.如果子节点是i,那么父结点是i/2)
2. 如何建堆(以建立大堆为例说明)
对于大根堆,调整方法为:若【根节点的关键字】小于【左右子女中关键字较大者】,则交换。(需要注意的是二叉树最后一个根节点可能没有右孩子)
我们知道,最后一个非终端结点(或者称为父结点)是(n-2)/2, 节点n-1是它的孩子,那么我们首先从这个作为根节点开始调整。
之后向前依次对各节点((n-2)/2 - 1)~ 0为根的子树调整,看该节点值是否大于其左右子节点的值,若不是,将左右子节点中较大值与之交换,交换后可能会破坏下一级堆,于是继续采用上述方法构建下一级的堆,直到以该节点为根的子树构成堆为止。
反复利用上述调整堆的方法建堆,直到根节点。这样,堆顶(整棵树的根节点)就是整个元素中的最大值。(如果是小堆的话,堆顶值就是整个元素中的最小值)
二、排序
将堆顶元素array[0](最大值)与堆低元素array[n-1]交换,那么此时,最大值就被放在了正确的位置(最后面),但此时,堆被破坏掉了,需要重现调整,那么就从堆顶元素开始自上往下开始按照前面建堆的方式调整使其继续保持大堆的性质,最后再重复这个步骤。
说明:实际上,如果想按从小到大(顺序)排列输出,那么应该建立大堆,否则,如果想按从大道小(逆序)排列输出,应该建立小堆。
虽然话是这么说,但估计很难让人明白对不对?^^
下面先上代码:
1 //调整比较 2 void HeapAdjust(int *A, int s, int m) 3 { 4 int i, rc = A[s]; 5 //s为父结点,从2*s+1左子树开始,下一轮循环又从左子树的左子树开始,看看是否需要继续往下调整 6 //因为调整了上一个父子结点后,很可能破坏了下一个父子结点的结构,所以要继续往下调整 7 for(i=2*s+1; i<=m-1; i=2*i+1) 8 { 9 //比较左右子树哪个大,取最大值与父结点比较,注意这里i+1<m,因为很可能没有右子树 10 if(i+1 < m && A[i] < A[i+1]) 11 { 12 ++i; 13 } 14 15 if(rc >= A[i]) 16 { 17 //如果父结点比较左右子树都大,那么可以退出了,目的达到了 18 break; 19 } 20 21 A[s] = A[i]; //交换父子的值,把子树的值赋值给父亲 22 s = i; 23 } 24 25 A[s] = rc; //把父亲的值赋值给子树 26 } 27 28 void BuildMaxHeapSort(int *A, int length) 29 { 30 int i = 0; 31 //注意这里从最后的一个非终端结点开始往上遍历 32 for(i=(length-2)/2; i>=0; i--) 33 { 34 HeapAdjust(A, i, length); 35 } 36 } 37 38 void MaxHeapSort(int *A, int length) 39 { 40 BuildMaxHeapSort(A, length); 41 42 int i, temp; 43 //这里从最后的元素开始与栈顶元素交换,把最大的值放最后面 44 for(i=length-1; i>0; i--) 45 { 46 temp = A[0]; 47 A[0] = A[i]; 48 A[i] = temp; 49 50 //从交换元素后的栈顶开始到倒数第二个开始重新调整 51 HeapAdjust(A, 0, i-1); 52 } 53 } 54 55 int main(int argc, char *argv[]) 56 { 57 int A[] = {25, 4, 1, 20, 2, 16}; 58 int len = sizeof(A)/sizeof(int); 59 60 MaxHeapSort(A, len); 61 62 int i = 0; 63 for(i=0; i<len; i++) 64 { 65 printf("%d ", A[i]); 66 } 67 printf(" "); 68 return 0; 69 }
测试结果输出:1 2 4 16 20 25
看了代码,是否想明白了呢?
若还不太明白,多看几遍吧,特别是关于最后一个非终端结点的理解以及下标从1还是0开始,对于理解整个过程都很重要。
然后画个图吧,一步一步跟踪代码的运行。
如有不明白的地方,请联系我。^^
重要参考:https://zhuanlan.zhihu.com/p/28862938