1. 程式人生 > >堆排序建堆複雜度為O(n)的證明

堆排序建堆複雜度為O(n)的證明

今天重溫堆排序,在網上搜了好多部落格文章,都是泛泛而談。有的只講了思路,有的直接貼上一份或幾份程式碼。好一點的對複雜度進行了分析,但是講到建堆複雜度,就一筆帶過或者說請參考演算法導論××頁。我覺得求建堆複雜度並不難,瞭解一下對於理解堆排序是有好處的,下文為求解過程。

堆排序就是藉助於堆的資料結構和堆的操作函式來完成排序功能的過程。堆的資料結構可以藉助於陣列表示出來並可以高效地進行堆的操作。我們為堆(最大堆)的元素從從上到下(從根到葉),從左到右進行1到n的編號,對應到陣列的相應Index。為了方便對應,這裡陣列的0位置空了出來。定義幾個操作:

  1. PARENT(i) : i/2 
  2. LEFT(i) : i<<1
  3. RIGHT(i) : (i<<1)+1

以上三個操作的意義很簡單,是屬於堆操作中的函式。還有兩個函式HEAPIFY , BUILD-HEAP,它們分別包裝了一種特殊的堆修正操作和初始化建堆操作。其中HEAPIFY是在i的左右子樹都是堆的前提下對以i為根的樹進行修正堆的操作。

用以上幾個堆的包裝函式就可以完成堆排序函式HEAP-SORT。
現在來分析建堆過程BUILD-HEAP:
BUILD-HEAP(int A[]){
	heapsize = A.length-1;
	for(int i=headsize/2;i>=1;i--){
		HEAPIFY(A,i);
	}
}
可以發現我們還需要分析一下HEAPIFY:
HEAPIFY(int A[],int i){
	int l = LEFT(i);
	int r = l + 1;
	int largest = i;
	if(l<=heapsize&&A[l]>A[i]){
		largest = l;
	}
	if(r<=heapsize&&A[r]>A[largest]){
		largest = r;
	}
	if(largest != i){
		swap(i,largest);
		HEAPIFY(A,largest);
	}
}

HEAPIFY對一層的比對交換所需時間是常數級的O(1),然後進入遞迴過程。設堆共有N個節點,則高度最多為LgN,因此HEAPIFY最多遞迴LgN,耗費時間O(LgN)。

再看BUILD-HEAP,迴圈HEAPIFY了N/2次,因此複雜度的上界很好理解,為(N/2)*LgN,即O(NLgN)。

但是這並不是一個緊繃的複雜度,仔細想想也知道根本沒進行(N/2)*LgN那麼多次。

所有的葉節點都不進行HEAPIFY,HEAPIFY是從高度為1的節點開始進行直到根為止。這時候我們需要理解HEAPIFY的執行過程,而不能單純的理解為LgN。對於高度為1的節點,至多替換髮生1次。對於高度為2的節點,至多替換髮生2次,以此類推,對於高度為h的節點,至多發生替換h次。我們知道,堆是滿樹,葉節點共有N/2個,它們的高度是0 。高度為1的節點正是他們的父節點,共有(N/2)/2個。高度為2的,類推有((N/2)/2)/2個。因此高度為h的共有N/(2的(h+1)次方)個。 好了,堆的高度總共只有0到LgN,現在每個高度的節點個數清楚,每個高度的每個節點至多發生的替換次數也清楚,則總共發生的替換數也就清楚了:

(N/(2的(h+1)次方)) * h 的求和     (h取值0~LgN)

N是常數, 化一下變成(N/2) * ( h / (2的h次方) ) (h取值0~LgN)
接下來就是一個級數求和問題了,學過高數的都應該知道怎麼求。求 ( h / (2的h次方) ) (h取值0~LgN):
設結果為S,則S = 1/2 + 2/(2的2次方)  + 3/(2的3次方) ... + LgN/ (2的LgN次方)。另S*(1/2) = 1/(2的2次方) + 2/(2的3次方) + 3/(2的4次方)...+LgN/(2的(LgN+1)次方)。
兩式錯位相減 有 S*(1/2) = 1/2 + 1/(2的2次方)  + 1/(2的3次方) ... + 1/ (2的LgN次方) - LgN/(2的(LgN+1)次方)。
右式前邊幾項為等比數列,最終化簡結果為S = 2 - (1/2)的(LgN-1)次方-LgN / ( 2的LgN次方)。
當N趨向於無窮大時,右式的二,三兩項都趨近於0,於是limS = 2。所以我們要求的BUILD-HEAP複雜度為O( (N/2) * S ) = O(N)。

從上述推導過程可以看出,重點在於根據BUILD-HEAP過程找出計算複雜度的算式,然後利用求級數,求極限的方法解出結果。其實最終還是迴歸了理解演算法和合理利用數學工具上。