堆——神奇的优先队列(下)

        接着上一Pa说。就是如何创建这个堆呢。能够从空的堆开始,而后依次往堆中插入每个元素,直到全部数都被插入(转移到堆中为止)。由于插入第i个元素的所用的时间是O(log i),因此插入全部元素的总体时间复杂度是O(NlogN),代码以下。
复制代码
n=0; for(i=1;i<=m;i++) { n++; h[ n]=a[ i]; //或者写成scanf("%d",&h[ n]);  siftup(); }
复制代码
 
        其实咱们还有更快得方法来创建堆。它是这样的。
 
        直接把99536722174612219252819214个数放入一个彻底二叉树中(这里咱们仍是用一个一维数组来存储彻底二叉树)。
        在这个棵彻底二叉树中,咱们从最后一个结点开始依次判断以这个结点为根的子树是否符合最小堆的特性。若是全部的子树都符合最小堆的特性,那么整棵树就是最小堆了。若是这句话没有理解不要着急,继续往下看。
 
        首先咱们从叶结点开始。由于叶结点没有儿子,因此全部以叶结点为根结点的子树(其实这个子树只有一个结点)都符合最小堆的特性(即父结点的值比子结点的值小)。这些叶结点压根就没有子节点,固然符合这个特性。所以全部叶结点都不须要处理,直接跳过。从第n/2个结点(n为彻底二叉树的结点总数,这里即7号结点)开始处理这棵彻底二叉树。注意彻底二叉树有一个性质:最后一个非叶结点是第n/2个结点。
 
        以7号结点为根的子树不符合最小堆的特性,所以要向下调整。
        同理以6号、5号和4结点为根的子树也不符合最小对的特性,都须要往下调整。
        下面是已经对7号、6号、5号和4结点为根结点的子树调整完毕以后的状态。
        固然目前这棵树仍然不符合最小堆的特性,咱们须要继续调整以3号结点为根的子树,即将3号结点向下调整。

 

        同理继续调整以2号结点为根的子树,最后调整以1号结点为根的子树。调整完毕以后,整棵树就符合最小堆的特性啦。
        小结一下这个建立堆的算法。把n个元素创建一个堆,首先我能够将这n个结点以自顶向下、从左到右的方式从1n编码。这样就能够把这n个结点转换成为一棵彻底二叉树。紧接着从最后一个非叶结点(结点编号为n/2)开始到根结点(结点编号为1),逐个扫描全部的结点,根据须要将当前结点向下调整,直到以当前结点为根结点的子树符合堆的特性。虽然讲起来起来很复杂,可是实现起来却很简单,只有两行代码以下:
for(i=n/2;i>=1;i--) siftdown(i);
        用这种方法来创建一个堆的时间复杂度是O(N),若是你感兴趣能够尝试本身证实一下,嘿嘿。
        堆还有一个做用就是堆排序,与快速排序同样堆排序的时间复杂度也是O(NlogN)。堆排序的实现很简单,好比咱们如今要进行从小到大排序,能够先创建最小堆,而后每次删除顶部元素并将顶部元素输出或者放入一个新的数组中,直到堆为空为止。最终输出的或者存放在新数组中数就已是排序好的了。
复制代码
//删除最大的元素 int deletemax() { int t; t=h[ 1];//用一个临时变量记录堆顶点的值 h[ 1]=h[ n];//将堆得最后一个点赋值到堆顶 n--;//堆的元素减小1 siftdown(1);//向下调整 return t;//返回以前记录的堆得顶点的最大值 }
复制代码

 

        建堆以及堆排序的完整代码以下:
复制代码
#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记录值较小的结点编号 if( h[ i] > h[ i*2] ) t=i*2; else t=i; //若是他有右儿子的状况下,再对右儿子进行讨论 if(i*2+1 <= n) { //若是右儿子的值更小,更新较小的结点编号 if(h[ t] > h[ i*2+1]) t=i*2+1; } //若是发现最小的结点编号不是本身,说明子结点中有比父结点更小的 if(t!=i) { swap(t,i);//交换它们,注意swap函数须要本身来写 i=t;//更新i为刚才与它交换的儿子结点的编号,便于接下来继续向下调整  } else flag=1;//则否说明当前的父结点已经比两个子结点都要小了,不须要在进行调整了  } } //创建堆的函数 void creat() { int i; //从最后一个非叶结点到第1个结点依次进行向上调整 for(i=n/2;i>=1;i--) { siftdown(i); } } //删除最大的元素 int deletemax() { int t; t=h[ 1];//用一个临时变量记录堆顶点的值 h[ 1]=h[ n];//将堆得最后一个点赋值到堆顶 n--;//堆的元素减小1 siftdown(1);//向下调整 return t;//返回以前记录的堆得顶点的最大值 } int main() { int i,num; //读入数的个数 scanf("%d",&num); for(i=1;i<=num;i++) scanf("%d",&h[ i]); n=num; //建堆  creat(); //删除顶部元素,连续删除n次,其实夜就是从大到小把数输出来 for(i=1;i<=num;i++) printf("%d ",deletemax()); getchar(); getchar(); return 0; }
复制代码

 

        能够输入如下数据进行验证
        14
        99 5 36 7 22 17 46 12 2 19 25 28 1 92
        运行结果是
        1 2 5 7 12 17 19 22 25 28 36 46 92 99

 

        固然堆排序还有一种更好的方法。从小到大排序的时候不创建最小堆而创建最大堆。最大堆创建好后,最大的元素在h[ 1]。由于咱们的需求是从小到大排序,但愿最大的放在最后。所以咱们将h[ 1]h[ n]交换,此时h[ n]就是数组中的最大的元素。请注意,交换后还需将h[ 1]向下调整以保持堆的特性。OK如今最大的元素已经归位,须要将堆的大小减1n--,而后再将h[ 1]h[ n]交换,并将h[ 1]向下调整。如此反复,直到堆的大小变成1为止。此时数组h中的数就已是排序好的了。代码以下:
复制代码
//堆排序 void heapsort() { while(n>1) { swap(1,n); n--; siftdown(1); } }
复制代码

 

完整的堆排序的代码以下,注意使用这种方法来进行从小到大排序须要创建最大堆。
复制代码
#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记录值较大的结点编号 if( h[ i] < h[ i*2] ) t=i*2; else t=i; //若是他有右儿子的状况下,再对右儿子进行讨论 if(i*2+1 <= n) { //若是右儿子的值更大,更新较小的结点编号 if(h[ t] < h[ i*2+1]) t=i*2+1; } //若是发现最大的结点编号不是本身,说明子结点中有比父结点更大的 if(t!=i) { swap(t,i);//交换它们,注意swap函数须要本身来写 i=t;//更新i为刚才与它交换的儿子结点的编号,便于接下来继续向下调整  } else flag=1;//则否说明当前的父结点已经比两个子结点都要大了,不须要在进行调整了  } } //创建堆的函数 void creat() { int i; //从最后一个非叶结点到第1个结点依次进行向上调整 for(i=n/2;i>=1;i--) { siftdown(i); } } //堆排序 void heapsort() { while(n>1) { swap(1,n); n--; siftdown(1); } } int main() { int i,num; //读入n个数 scanf("%d",&num); for(i=1;i<=num;i++) scanf("%d",&h[ i]); n=num; //建堆  creat(); //堆排序  heapsort(); //输出 for(i=1;i<=num;i++) printf("%d ",h[ i]); getchar(); getchar(); return 0; }
复制代码

 

        能够输入如下数据进行验证
        14
        99 5 36 7 22 17 46 12 2 19 25 28 1 92
        运行结果是
        1 2 5 7 12 17 19 22 25 28 36 46 92 99

 

        OK,最后仍是要总结一下。像这样支持插入元素和寻找最大(小)值元素的数据结构称之为优先队列。若是使用普通队列来实现这个两个功能,那么寻找最大元素须要枚举整个队列,这样的时间复杂度比较高。若是已排序好的数组,那么插入一个元素则须要移动不少元素,时间复杂度依旧很高。而堆就是一种优先队列的实现,能够很好的解决这两种操做。
 
        另外Dijkstra算法中每次找离源点最近的一个顶点也能够用堆来优化,使算法的时间复杂度降到O((M+N)logN)。堆还常常被用来求一个数列中第K大的数。只须要创建一个大小为K的最小堆,堆顶就是第K大的数。若是求一个数列中第K小的数,只最须要创建一个大小为K的最大堆,堆顶就是第K小的数,这种方法的时间复杂度是O(NlogK)。固然你也能够用堆来求前K大的数和前K小的数。你还能想出更快的算法吗?有兴趣的同窗能够去阅读《编程之美》第二章第五节。
 
        堆排序算法是由J.W.J. Williams1964年发明,他同时描述了如何使用堆来实现一个优先队列。同年,由Robert WFloyd提出了创建堆的线性时间算法。
相关文章
相关标签/搜索