結論:
堆初始化的時間復雜度為 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)。
綜上所述,堆初始化在空間上,時間上均更有優勢,所以是有必要的。
