结论:
堆初始化的时间复杂度为 O(N)
插入成堆的时间复杂度为 O(N Log N)
!!!阅读前需先了解完全二叉树,堆排序算法,不清楚移步
堆排序伪代码:
HEAPSORT( A )
BUILD_MAX_HEAP(A); //堆初始化,本文讨论的主题
for i=A.length down to 2
exchange A[1] with A[i]
A.length=A.length-1
MAX_HEAPIFY(A,1) //自上而下维护堆
嘿!堆为什么要初始化?
一日,在实现堆排序算法时,室友好奇的一问:你这个数组为什么要先初始化成堆呀?这么复杂,声明一个空堆,不断的把数组里的元素插入进去不就好了吗?
欸!好像是这么一回事呢。那堆排序算法里的这个初始化是不是太多余了?
首先,先说明维护堆的两个操作,作为接下来分析的基础:
typedef int* HEAP; //int为元素的堆
1.自上而下的维护一棵子树为大根堆,在这里将该方法命名为:adjust_down(HEAP heap,int pos,int len);算法执行次数取决于该节点到叶子节点的距离。
//算法演示 void adjust_down(HEAP heap,int pos,int len) { heap[0]=heap[pos]; for(int i=pos<<1;i<=len;i<<=1){ if(i<len&&heap[i+1]>heap[i]) i++; if(heap[i]>heap[0]) heap[pos]=heap[i],pos=i; else break; } heap[pos]=heap[0]; }
2.自下而上的将一个元素插入到堆中合适的位置,在这里将该方法命名为:adjust_up(HEAP heap,int pos);算法执行次数取决于该节点到根的距离。
//算法演示 void adjust_up(HEAP heap,int pos) { heap[0]=heap[pos]; int parent=pos/2; while(parent>0&&heap[parent]<heap[0]){ heap[pos]=heap[parent]; pos=parent,parent/=2; } heap[pos]=heap[0]; }
室友说的对吗?
从描述上来看,首先空堆是满足条件的,在每次插入前堆都是大根堆,那么插入以后执行adjust_up(),最后确实生成了一棵大根堆。
而堆的初始化过程为:从元素的第len/2个元素开始到第一个元素,不断的调用dajust_down(),最后也生成一个大根堆。
两个方法最后都生成了大根堆。
那为什么还要有堆初始化的过程呢?
既然能实现同样的功能,那效率上是否有差异呢?
最直观的,从空间上看,因为堆的实现方式是数组,如果先申请一个堆,在不断的往里面插入,那么堆的空间与数组空间一样大,需要相当于两个数组的容量。
如果采用在数组上初始化,则不需要多余的空间。
从时间上来看,堆初始化是自上而下,插入成堆是自下而上;
堆初始化:因为每个节点需要的比较的次数取决该节点到叶子节点的距离(原因参考代码:adjust_down() )。
1)设树的深度从0开始计数,树的深度为K,,结点个数为N。
2)深度为K-i的结点,需要的比较次数为 i。
3)除最后一层节点可能不满以外,深度为K-i的那一层结点总数为$2^{K-i}$
故总的比较次数 $S=\sum_{i=1}^{K} {2^{K-i}*i}$
则$2*S=\sum_{i=1}^{K} {2^{K-i+1}*i}$
得$S=2*S-S=\sum_{i=1}^{K} {2^{K}}+K = 2^{K+1}-2+K$
因为$\sum_{i=1}^{K} {2^{K}} \rightarrow N ,$
故初始化的时间复杂度为 O(N),
插入成堆:因为每个结点需要的比较次数取决于该结点到根的距离(原因参考代码:adjust_up() )。
1)分析时,以一颗满二叉树为代表,以简化分析过程,其他相关参数如上定义。
2)深度为i的结点,需要的比较次数为 i。
3)深度为i的那一层结点总数为$2^{i}$
故总的比较次数 $S=\sum_{i=1}^{K} {2^{i}*i}$
则$2*S=\sum_{i=1}^{K} {2^{i+1}*i}$
得$S=2*S-S=(K-1)*2^{K+1}+2^K-2$
因为$\sum_{i=1}^{K} {2^{K}} \rightarrow N ,$
故插入成堆的时间复杂度为O(N Log N)。
综上所述,堆初始化在空间上,时间上均更有优势,所以是有必要的。