接着上一篇说。就是如何建立这个堆呢。可以从空的堆开始,然后依次往堆中插入每一个元素,直到所有数都被插入(转移到堆中为止)。因为插入第i个元素的所用的时间是O(log i),所以插入所有元素的整体时间复杂度是O(NlogN),代码如下。
1 2 3 4 5 6 7 |
n=0; for(i=1;i<=m;i++) { n++; h[ n]=a[ i]; //或者写成scanf("%d",&h[ n]); siftup(); } |
其实我们还有更快得方法来建立堆。它是这样的。
直接把99、5、36、7、22、17、46、12、2、19、25、28、1和92这14个数放入一个完全二叉树中(这里我们还是用一个一维数组来存储完全二叉树)。
在这个棵完全二叉树中,我们从最后一个结点开始依次判断以这个结点为根的子树是否符合最小堆的特性。如果所有的子树都符合最小堆的特性,那么整棵树就是最小堆了。如果这句话没有理解不要着急,继续往下看。
首先我们从叶结点开始。因为叶结点没有儿子,所以所有以叶结点为根结点的子树(其实这个子树只有一个结点)都符合最小堆的特性(即父结点的值比子结点的值小)。这些叶结点压根就没有子节点,当然符合这个特性。因此所有叶结点都不需要处理,直接跳过。从第n/2个结点(n为完全二叉树的结点总数,这里即7号结点)开始处理这棵完全二叉树。注意完全二叉树有一个性质:最后一个非叶结点是第n/2个结点。
以7号结点为根的子树不符合最小堆的特性,因此要向下调整。
同理以6号、5号和4结点为根的子树也不符合最小对的特性,都需要往下调整。
下面是已经对7号、6号、5号和4结点为根结点的子树调整完毕之后的状态
当然目前这棵树仍然不符合最小堆的特性,我们需要继续调整以3号结点为根的子树,即将3号结点向下调整。
同理继续调整以2号结点为根的子树,最后调整以1号结点为根的子树。调整完毕之后,整棵树就符合最小堆的特性啦。
小结一下这个创建堆的算法。把n个元素建立一个堆,首先我可以将这n个结点以自顶向下、从左到右的方式从1到n编码。这样就可以把这n个结点转换成为一棵完全二叉树。紧接着从最后一个非叶结点(结点编号为n/2)开始到根结点(结点编号为1),逐个扫描所有的结点,根据需要将当前结点向下调整,直到以当前结点为根结点的子树符合堆的特性。虽然讲起来起来很复杂,但是实现起来却很简单,只有两行代码如下:
1 2 |
for(i=n/2;i>=1;i--) siftdown(i); |
用这种方法来建立一个堆的时间复杂度是O(N),如果你感兴趣可以尝试自己证明一下,嘿嘿。
堆还有一个作用就是堆排序,与快速排序一样堆排序的时间复杂度也是O(NlogN)。堆排序的实现很简单,比如我们现在要进行从小到大排序,可以先建立最小堆,然后每次删除顶部元素并将顶部元素输出或者放入一个新的数组中,直到堆为空为止。最终输出的或者存放在新数组中数就已经是排序好的了。
1 2 3 4 5 6 7 8 9 10 |
//删除最大的元素 int deletemax() { int t; t=h[ 1];//用一个临时变量记录堆顶点的值 h[ 1]=h[ n];//将堆得最后一个点赋值到堆顶 n--;//堆的元素减少1 siftdown(1);//向下调整 return t;//返回之前记录的堆得顶点的最大值 } |
建堆以及堆排序的完整代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 |
#include <stdio.h> int h[ 101];//用来存放堆的数组 int n;//用来存储堆中元素的个数,也就是堆的大小 //交换函数,用来交换堆中的两个元素的值 void swap(int x,int y) { int t; t=h[ x]; h[ x]=h[ y]; h[ y]=t; } //向下调整函数 void siftdown(int i) //传入一个需要向下调整的结点编号i,这里传入1,即从堆的顶点开始向下调整 { int t,flag=0;//flag用来标记是否需要继续向下调整 //当i结点有儿子的时候(其实是至少有左儿子的情况下)并且有需要继续调整的时候循环窒执行 while( i*2<=n && flag==0 ) { //首先判断他和他左儿子的关系,并用t记录值较小的结点编号 就是如何建立这个堆呢。可以从空的堆开始,然后依次往堆中插入每一个元素,直到所有数都被插入(转移到堆中为止)。因为插入第i个元素的所用的时间是O(log i),所以插入所有元素的整体时间复杂度是O(NlogN),代码如下。
其实我们还有更快得方法来建立堆。它是这样的。 直接把99、5、36、7、22、17、46、12、2、19、25、28、1和92这14个数放入一个完全二叉树中(这里我们还是用一个一维数组来存储完全二叉树)。 在这个棵完全二叉树中,我们从最后一个结点开始依次判断以这个结点为根的子树是否符合最小堆的特性。如果所有的子树都符合最小堆的特性,那么整棵树就是最小堆了。如果这句话没有理解不要着急,继续往下看。 首先我们从叶结点开始。因为叶结点没有儿子,所以所有以叶结点为根结点的子树(其实这个子树只有一个结点)都符合最小堆的特性(即父结点的值比子结点的值小)。这些叶结点压根就没有子节点,当然符合这个特性。因此所有叶结点都不需要处理,直接跳过。从第n/2个结点(n为完全二叉树的结点总数,这里即7号结点)开始处理这棵完全二叉树。注意完全二叉树有一个性质:最后一个非叶结点是第n/2个结点。 以7号结点为根的子树不符合最小堆的特性,因此要向下调整。
同理以6号、5号和4结点为根的子树也不符合最小对的特性,都需要往下调整。 下面是已经对7号、6号、5号和4结点为根结点的子树调整完毕之后的状态 当然目前这棵树仍然不符合最小堆的特性,我们需要继续调整以3号结点为根的子树,即将3号结点向下调整。 同理继续调整以2号结点为根的子树,最后调整以1号结点为根的子树。调整完毕之后,整棵树就符合最小堆的特性啦。 小结一下这个创建堆的算法。把n个元素建立一个堆,首先我可以将这n个结点以自顶向下、从左到右的方式从1到n编码。这样就可以把这n个结点转换成为一棵完全二叉树。紧接着从最后一个非叶结点(结点编号为n/2)开始到根结点(结点编号为1),逐个扫描所有的结点,根据需要将当前结点向下调整,直到以当前结点为根结点的子树符合堆的特性。虽然讲起来起来很复杂,但是实现起来却很简单,只有两行代码如下:
用这种方法来建立一个堆的时间复杂度是O(N),如果你感兴趣可以尝试自己证明一下,嘿嘿。 堆还有一个作用就是堆排序,与快速排序一样堆排序的时间复杂度也是O(NlogN)。堆排序的实现很简单,比如我们现在要进行从小到大排序,可以先建立最小堆,然后每次删除顶部元素并将顶部元素输出或者放入一个新的数组中,直到堆为空为止。最终输出的或者存放在新数组中数就已经是排序好的了。
建堆以及堆排序的完整代码如下:
|