資料結構和演算法面試題系列-二叉堆
本文要描述的堆是二叉堆。二叉堆是一種陣列物件,可以被視為一棵完全二叉樹,樹中每個結點和陣列中存放該結點值的那個元素對應。樹的每一層都是填滿的,最後一層除外。二叉堆可以用於實現堆排序,優先順序佇列等。本文程式碼地址在 這裡 。
1 二叉堆定義
使用陣列來實現二叉堆,二叉堆兩個屬性,其中 LENGTH(A)
表示陣列 A
的長度,而 HEAP_SIZE(A)
則表示存放在A中的堆的元素個數,其中 LENGTH(A) <= HEAP_SIZE(A)
,也就是說雖然 A[0,1,...N-1]
都可以包含有效值,但是 A[HEAP_SIZE(A)-1]
之後的元素不屬於相應的堆。
二叉堆對應的樹的根為 A[0]
,給定某個結點的下標 i ,可以很容易計算它的父親結點和兒子結點。 注意在後面的示例圖中我們標註元素是從1開始計數的,而實現程式碼中是從0開始計數。
#define PARENT(i) ( i > 0 ? (i-1)/2 : 0) #define LEFT(i) (2 * i + 1) #define RIGHT(i) (2 * i + 2) 複製程式碼
注:堆對應的樹每一層都是滿的,所以一個高度為 h
的堆中,元素數目最多為 1+2+2^2+...2^h = 2^(h+1) - 1
(滿二叉樹),元素數目最少為 1+2+...+2^(h-1) + 1 = 2^h
。 由於元素數目 2^h <= n <= 2^(h+1) -1
,所以 h <= lgn < h+1
,因此 h = lgn
。即一個包含n個元素的二叉堆高度為 lgn
。
2 保持堆的性質
本文主要建立一個最大堆,最小堆原理類似。為了保持堆的性質, maxHeapify(int A[], int i)
函式讓堆陣列 A
在最大堆中下降,使得以 i
為根的子樹成為最大堆。
void maxHeapify(int A[], int i, int heapSize) { int l = LEFT(i); int r = RIGHT(i); int largest = i; if (l <= heapSize-1 && A[l] > A[i]) { largest = l; } if (r <= heapSize-1 && A[r] > A[largest]) { largest = r; } if (largest != i) { // 最大值不是i,則需要交換i和largest的元素,並遞迴呼叫maxHeapify。 swapInt(A, i, largest); maxHeapify(A, largest, heapSize); } } 複製程式碼
-
在演算法每一步裡,從元素
A[i]
和A[left]
以及A[right]
中選出最大的,將其下標存在largest
中。如果A[i]
最大,則以i
為根的子樹已經是最大堆,程式結束。 -
否則,
i
的某個子結點有最大元素,將A[i]
與A[largest]
交換,從而使i及其子女滿足最大堆性質。此外,下標為largest
的結點在交換後值變為A[i]
,以該結點為根的子樹又有可能違反最大堆的性質,所以要對該子樹遞迴呼叫maxHeapify()
函式。
當 maxHeapify()
函式作用在一棵以 i
為根結點的、大小為 n
的子樹上時,執行時間為調整 A[i]
、 A[left]
、 A[right]
的時間 O(1)
,加上對以 i
為某個子結點為根的子樹遞迴呼叫 maxHeapify
的時間。 i
結點為根的子樹大小最多為 2n/3
(最底層剛好半滿的時候),所以可以推得 T(N) <= T(2N/3) + O(1)
,所以 T(N)=O(lgN)
。
下圖是一個執行 maxHeapify(heap, 2)
的例子。 A[] = {16, 4, 10, 14, 7, 9, 3, 2, 8, 1}
,堆大小為 10
。

3 建立最大堆
我們可以知道,陣列 A[0, 1, ..., N-1]
中, A[N/2, ..., N-1]
的元素都是樹的葉結點。如上面圖中的 6-10
的結點都是葉結點。每個葉子結點可以看作是隻含一個元素的最大堆,因此我們只需要對其他的結點呼叫 maxHeapify()
函式即可。
void buildMaxHeap(int A[], int n) { int i; for (i = n/2-1; i >= 0; i--) { maxHeapify(A, i, n); } } 複製程式碼
之所以這個函式是正確的,我們需要來證明一下,可以使用迴圈不變式來證明。
迴圈不變式:在for迴圈開始前,結點 i+1、i+2...N-1
都是一個最大堆的根。
初始化:for迴圈開始迭代前, i = N/2-1
, 結點 N/2, N/2+1, ..., N-1
都是葉結點,也都是最大堆的根。
保持:因為結點 i
的子結點標號都比 i
大,根據迴圈不變式的定義,這些子結點都是最大堆的根,所以呼叫 maxHeapify()
後, i
成為了最大堆的根,而 i+1, i+2, ..., N-1
仍然保持最大堆的性質。
終止:過程終止時,i=0,因此結點 0, 1, 2, ..., N-1
都是最大堆的根,特別的,結點0就是一個最大堆的根。

雖然每次呼叫 maxHeapify()
時間為 O(lgN)
,共有 O(N)
次呼叫,但是說執行時間是 O(NlgN)
是不確切的,準確的來說,執行時間為 O(N)
,這裡就不證明了,具體證明過程參見《演算法導論》。
4 堆排序
開始用 buildMaxHeap()
函式建立一個最大堆,因為陣列最大元素在 A[0]
,通過直接將它與 A[N-1]
互換來達到最終正確位置。去掉 A[N-1]
,堆的大小 heapSize
減1,呼叫 maxHeapify(heap, 0, --heapSize)
保持最大堆的性質,直到堆的大小由N減到1。
void heapSort(int A[], int n) { buildMaxHeap(A, n); int heapSize = n; int i; for (i = n-1; i >= 1; i--) { swapInt(A, 0, i); maxHeapify(A, 0, --heapSize); } } 複製程式碼
5 優先順序佇列
最後實現一個最大優先順序佇列,主要有四種操作,分別如下所示:
insert(PQ, key) maximum(PQ) extractMax(PQ) increaseKey(PQ, i, key)
這裡定義一個結構體 PriorityQueue
便於操作。
typedef struct PriorityQueue { int capacity; int size; int elems[]; } PQ; 複製程式碼
最終優先順序佇列的操作實現程式碼如下:
/** * 從陣列建立優先順序佇列 */ PQ *newPQ(int A[], int n) { PQ *pq = (PQ *)malloc(sizeof(PQ) + sizeof(int) * n); pq->size = 0; pq->capacity = n; int i; for (i = 0; i < pq->capacity; i++) { pq->elems[i] = A[i]; pq->size++; } buildMaxHeap(pq->elems, pq->size); return pq; } int maximum(PQ *pq) { return pq->elems[0]; } int extractMax(PQ *pq) { int max = pq->elems[0]; pq->elems[0] = pq->elems[--pq->size]; maxHeapify(pq->elems, 0, pq->size); return max; } PQ *insert(PQ *pq, int key) { int newSize = ++pq->size; if (newSize > pq->capacity) { pq->capacity = newSize * 2; pq = (PQ *)realloc(pq, sizeof(PQ) + sizeof(int) * pq->capacity); } pq->elems[newSize-1] = INT_MIN; increaseKey(pq, newSize-1, key); return pq; } void increaseKey(PQ *pq, int i, int key) { int *elems = pq->elems; elems[i] = key; while (i > 0 && elems[PARENT(i)] < elems[i]) { swapInt(elems, PARENT(i), i); i = PARENT(i); } } 複製程式碼