buildMaxHeap方法
buildMaxHeap方法的流程簡單概括起來就是一句話,從A.length / 2一直到根結點進行maxHeapify調整。下面是圖解。
public static void maxHeapify(int[] a, int i, int length) { int l = left(i); int r = right(i); int largest = i; while(true) { if(l < length && a[l] > a[i]) largest = l; if(r < length && a[r] > a[largest]) largest = r; if(i != largest) swap(a, i, largest); else break; i = largest; l = left(largest); r = right(largest); } } public static void buildMaxHeap(int[] a) { for(int i = a.length / 2; i >= 0; i--) maxHeapify(a, i, a.length); }
運行時間分析
粗粗來看前面buildmaxheap的時間復雜度,每次maxHeapify調整需要的時間為lg(n), 總共要遍歷的元素有N/2個,所以大致的運行時間復雜度為O(nlgn).
如果我們更進一步分析,會發現它的實際情況會更理想一點。首先一個,我們從第a.length/2個元素開始執行maxHeapify,最開始這一層的元素只有一個子結點,也就是說,就算要調整,頂多一次就搞定了,不需要走lgn這么多步。
要做進一步的分析,我們可以先思考一下我們要建的這個完全二叉樹堆的幾個特性。以如下圖為例:
我們看這棵二叉樹,它必須保證每一層填滿之后才能去填充下一層。而且,如果從根結點開始計數,往下第i層的元素如果不是最后一層的話,這一層的元素數量為2**i(2的i次方)。這樣,對於一棵高為h的二叉樹,它的所有結點數目就等於前面完全填滿的層元素加上最下面一層的元素。
為什么要把他們分開來計數呢?是因為最下面一層的元素有一個變動的范圍,作為一棵高度為h的樹,最下面一層的元素最少可以是1,最大可以是把整層填充滿,也就是2**(h+1)。這樣,他們求和的結果就是最少為2**h,最大為2**(h+1)。
所以假設堆的元素數量為n的話,我們就可以推出:
結合這一步分析,我們可以得到: h <= lgn < h + 1。
結論1:
我們可以發現一個n個元素的樹,它的高度相當於logn(向下取整)。
我們再來看我們分析的第二個結論。對應樹每一個高度的一層,該有多少個元素呢?假設高度為1的那一層他們的元素個數為k,那么他們的訪問時間復雜度為O(k)。根據前面的分析,我們還發現一個情況,就是如果從根結點開始計數,往下第i層的元素如果不是最后一層的話,這一層的元素數量為2**i(2的i次方)。正好這個第i層就相當於樹的總高度減去當前層的結點的高度。假設第i層的高度為h,那么也就是i = lgn - h。
結論2:
這一層的元素數量為:
那么他們所有元素的運算時間總和為如下:
根據如下公式:
則有:
現在,我們發現,buildMaxHeap方法的時間復雜度實際上為O(n).
maxHeapInsert方法
maxHeapInsert方法和前面的辦法不一樣。它可以假定我們事先不知道有多少個元素,通過不斷往堆里面插入元素進行調整來構建堆。
它的大致步驟如下:
1. 首先增加堆的長度,在最末尾的地方加入最新插入的元素。
2. 比較當前元素和它的父結點值,如果比父結點值大,則交換兩個元素,否則返回。
3. 重復步驟2.
這個過程對應的代碼實現如下:
public void heapIncreaseKey(int i, int key) throws Exception { if(key < a[i]) throw new Exception("new key is small than current key"); a[i] = key; while(i > 0 && a[parent(i)] < a[i]) { swap(i, parent(i)); i = parent(i); } } public void maxHeapInsert(int key) throws Exception { heapSize++; a[heapSize - 1] = Integer.MIN_VALUE; heapIncreaseKey(heapSize - 1, key); }
這里的parent()方法是用來求當前結點的父結點。詳細的實現可以參考后面附件里的代碼。
這里,我們也可以分析一下插入建堆的時間復雜度。我們先看最理想的情況,假設每次插入的元素都是嚴格遞減的,那么每個元素只需要和它的父結點比較一次。那么其最優情況就是n。
對於最壞的情況下,每次新增加一個元素都需要調整到它的根結點。而這個長度為lgn。那么它的時間復雜度為如下公式:
這樣,插入建堆的時間復雜度為nlgn。
總結
常用的建堆方法主要用於堆元素已經確定好的情況,而插入建堆的過程主要用於動態的增加元素來建堆。插入建堆的過程也常用於建立優先隊列的應用。這些可以根據具體的時間情況來選取。