堆排序中首先須要作的就是建堆,廣爲人知的是建堆複雜度才O(n),它的證實過程涉及到高等數學中的級數或者機率論,不過證實總體來說是比較易懂的。算法
堆排過程
代碼以下api
void print(vector<int> &arr) { for(auto n: arr) printf("%d\t", n); cout<<endl; } // 以arr[n]爲根的子樹,將arr[n]向下調整至合適位置 void Heapify(vector<int> &arr, int size, int n) { int L = n*2+1, R = L+1; if(L>=size) return ;//無孩 int big = arr[L]; // 取兩孩之大者 if(R<size) big = max(big, arr[R]); if(arr[n]>=big) return ; //無需調整 int c = L; // 欲與父交換位置的孩子 if(big!=arr[L]) c = R; swap(arr[n], arr[c]); Heapify(arr, size, c); } // 小根堆 void BuildHeap(vector<int> &arr) { int last = (arr.size()-1)/2; for(int i=last; i>=0; i--) { Heapify(arr, arr.size(), i); } } // 順便排序 void Sort(vector<int> &arr) { int size = arr.size(); for(int i=size-1; i>0; i--) { swap(arr[0], arr[i]); Heapify(arr, i, 0); //調整一下arr[0] } } int main() { vector<int> vect{9, 10, 6, 3, 1, 6, 2, 8, 4}; print(vect); //排序前 BuildHeap(vect); //建堆 Sort(vect); //排序 print(vect); //排序後 return 0; }
建堆的過程就是從最後一個分支結點開始逐層向上遍歷,將結點向下調整至合適的位置,以不至於破壞原來的堆。好比上圖,遍歷的結點編號依次爲3
2
1
,首先調整以3
爲根的子樹成堆,其次是以2
爲根的子樹成堆,最後是以1
爲根的子樹成堆。至此建堆完成,複雜度O(n)。ui
注意:建堆不能寫成以下這樣,這樣的建堆算法複雜度是O(nlogn),雖然不會影響堆排序的複雜度O(nlogn),可是實現其餘算法時就很不利了。spa
// 將arr[n]向上調整至合適位置 void AdjustHeap(vector<int> &arr, int n) { if(n<=0) return ; if(arr[(n-1)/2] > arr[n]) { //與父結點比較 swap(arr[(n-1)/2], arr[n]); AdjustHeap(arr, (n-1)/2); //遞歸調整 } } // 小根堆 void BuildHeap(vector<int> &arr) { for(int i=1; i<arr.size(); i++) { AdjustHeap(arr, i); } }
複雜度計算
從直觀上看,Heapify()
的遞歸深度最多爲${log_n}$,故它的複雜度上限爲O(logn)。而BuildHeap()
中的循環爲${ \frac{n}{2} }$次,故它的複雜度爲O(nlogn),但這不是它的實際複雜度,而是一個估算的上界,它極可能永遠達不到這個上界。爲了方便計算,考慮結點數量爲n,高度爲h的滿二叉樹,所以${2^h-1 = n}$,即${h = log_2{(n+1)}}$。code
第幾層 | 最多調整次數 | 層調整次數累計 |
---|---|---|
${h}$ | $0$ | ${2^{h-1}*0}$ |
${h-1}$ | $1$ | ${2^{h-2}*1}$ |
${h-2}$ | $2$ | ${2^{h-3}*2}$ |
$\vdots$ | $\vdots$ | $\vdots$ |
$3$ | ${ h-3 }$ | ${2^{2}*(h-3)}$ |
$2$ | ${ h-2 }$ | ${2^{1}*(h-2)}$ |
$1$ | ${ h-1 }$ | ${2^{0}*(h-1)}$ |
將最右邊一列累加起來就是建堆的調整次數,則建堆的調整次數${S(n)}$爲排序
$${S(n) = 12^{h-2}+22^{h-3}+}\cdots {+(h-2)*2^1 +(h-1)*2^0}$$ $${=2^{h-1} * ( \frac{1}{2^{1}} +\frac{2}{2^{2}} +\frac{3}{2^{3}} +}\cdots {+\frac{h-2}{2^{h-2}} +\frac{h-1}{2^{h-1}} )} \tag{1}$$遞歸
則 $${\frac{1}{2} S(n) = 2^{h-1} *(\frac{1}{2^2} + \frac{2}{2^3} + \frac{3}{2^4} + }\cdots{+\frac{h-2}{2^{h-1}} +\frac{h-1}{2^h})} \tag{2}$$數學
將(1)式減去(2)式得 $${S(n)-\frac{1}{2}S(n) = 2^{h-1} * (\frac{1}{2^1} + \frac{1}{2^2} + \frac{1}{2^3} + }\cdots{+\frac{1}{2^{h-2}} + \frac{1}{2^{h-1}} -\frac{h-1}{2^h} )} $$ $${ = 2^{h-1} * (\frac{1}{1-\frac{1}{2}}-1-\frac{h-1}{2^h} ) } \tag{3}$$ $${ =2^{h-1} * ( 1 - \frac{h-1}{2^h})}$$ $${ =2^{h-1}} $$table
又因 ${ n = 2^h-1 }$,故有 $${S(n) = 2^h = \frac{n+1}{2}}$$ast
注意:上面列式均是當n趨於無窮大時的計算,且(3)式是由級數的直接變換所得。其餘的證實思路還有用機率的,就不寫了。
寫公式寫到頭皮發麻,寫錯n次了,若是錯漏請不吝指正,感謝!