這個系列是我多年前找工做時對數據結構和算法總結,其中有基礎部分,也有各大公司的經典的面試題,最先發布在CSDN。現整理爲一個系列給須要的朋友參考,若有錯誤,歡迎指正。本系列完整代碼地址在 這裏。git
本文要描述的堆是二叉堆。二叉堆是一種數組對象,能夠被視爲一棵徹底二叉樹,樹中每一個結點和數組中存放該結點值的那個元素對應。樹的每一層都是填滿的,最後一層除外。二叉堆能夠用於實現堆排序,優先級隊列等。本文代碼地址在 這裏。github
使用數組來實現二叉堆,二叉堆兩個屬性,其中 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
。api
本文主要創建一個最大堆,最小堆原理相似。爲了保持堆的性質,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
爲根的子樹已是最大堆,程序結束。bash
不然,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
。函數
咱們能夠知道,數組 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+一、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)
,這裏就不證實了,具體證實過程參見《算法導論》。
開始用 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);
}
}
複製代碼
最後實現一個最大優先級隊列,主要有四種操做,分別以下所示:
insert(PQ, key)
:將 key 插入到隊列中。maximum(PQ)
: 返回隊列中最大關鍵字的元素extractMax(PQ)
:去掉並返回隊列中最大關鍵字的元素increaseKey(PQ, i, key)
:將隊列 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);
}
}
複製代碼