[data structure] heap 堆
- 定義:
1 (二叉) 堆是一顆完全二叉樹。
2 任何一個節點都小於它的後裔節點。(最小堆),相應的,最大堆中的任何一個節點大於它的後裔節點。
由於二叉堆是一個完全二叉樹,因此對於二叉堆中的任何一個節點i,它的子節點的編號為:
A[ left ] = A[ 2*i + 1 ] ; A[ right ] = A [ 2*i + 2 ] (注:這是與實際實現時相對應的,比如如下的堆:)
由於堆的完全二叉樹的性質,我們使用陣列來表示堆即可。如下圖所示:
左圖是一個大頂堆,堆頂的元素是最大的。下方的陣列是與該堆對應的。(注:上述的陣列的有序是純屬偶然的。)
首要的,堆的操作最重要的性質便是保持堆序性。
- 插入元素
向堆中加入新的元素i,輸入:要新增的元素elem。使得加入後依然是堆。簡單的示意圖如下:
插入元素15,那麼需要判斷15是否破壞了堆序性,即是否該元素放在了正確的位置,顯然 15 > 10,不滿足堆序性,因此15上濾,得到右圖,由於 15 < 18,說明15到達了正確的位置,因此成功插入了元素15.
- 保持堆序性:
這是堆的所有操作的最重要的一個函式:通過遞迴的比較當前元素與它的左右子節點,決定是否將該元素下濾,直到到達正確的位置。
C++程式碼如下:
//max heapify for the i-th elem in A void max_heapify(int A[], int i, int heap_size) { int largest = i; int left = 2*i+1; int right = 2*i+2; if(left < heap_size && A[left] > A[largest]) { largest = left; } if(right < heap_size && A[right] > A[largest]) { largest = right; } if(largest != i) { swap(A[i],A[largest]); max_heapify(A,largest,heap_size); } }
- 建堆
給定陣列A,要求將A變成一個堆,也就是說逐漸改變陣列A中的元素位置,使得陣列保持堆序性。
假設給定陣列A = { 3 13 2 8 18 10 },要將該陣列構建成一個最大堆。
首先,在構建最大堆之前,必須明確:
1 由完全二叉樹的性質,陣列的至少一半的元素為葉子節點:
一個簡單的例子:對於一顆整的完全二叉樹而言,第 i 層的元素個數為 2^(i-1),(根節點為第0層!!注意與平時樹的高度的區別)。那麼有如下示意圖:
因此,前i層的節點總數為:
1 + 2 + 2^2 + ... + 2^(i-1) = 2^i - 1
第i+1層(即葉子節點層)的節點個數為2^i ,因此葉子節點的個數大約為總節點個數的一半。
另一方面想,由於完全二叉樹中父節點與子節點之間的位置關係,也可以知道對於某個節點i而言,如果它的右孩子編號大於整個陣列的大小,那麼顯然節點i是最後一箇中間節點,它後面的節點應該全部為葉子節點。即
2*i+2 = len, 那麼 i = ( len - 2 ) / 2。因此基於此我們建堆的過程只需要考慮陣列中的前一半的元素,程式碼如下:
void build_max_heap(int A[], int len)
{
for(int i = len/2 ; i >= 0 ; i--)
{
max_heapify(A,i,len);
}
}
- 堆排序:
由於每一次從堆中找出最大的元素(大頂堆)是很直接的,直接取出對頂元素即可,那麼在刪除堆頂的元素之後:
1 堆中的元素個數會減一
2 堆頂空缺,需要將剩下的最大的元素放置在堆頂
如下的簡單示意圖:
按照上述一直到堆中的元素為0,那麼陣列中的元素也就有序了(從小到大有序)。
程式碼如下:
void heapSort(int A[], int len)
{
build_max_heap(A,len);
//printHeap(A,len);
for(int i = len-1; i >= 0 ; --i)
{
swap(A[i],A[0]);
max_heapify(A,0,i);
}
}
因此,堆排序實現完畢。
注:如果你是要將陣列從大到小的排序,那麼你應該建立的是小頂堆,否則為大頂堆。
堆的各種操作大致都是堆的高度O(logn)量級的。
總結:堆是很重要的資料結構,在任務優先順序排程方面可作為優先順序佇列來使用,每次選擇優先順序最高的任務。
堆的操作還有很多,比如改變某個關鍵字的值,或者是刪除中間的某個關鍵字。值得一提的是,堆排序是一種in-place排序,複雜度為O(nlogn)。
補充:由於在實現堆的時候,老是要去畫二叉樹,於是自己實現了一個簡陋的只針對於完全二叉樹的程式:
static void printSpace(int number)
{
while(number--)cout << " ";
}
void printHeap(int A[], int len)
{
if(len == 0 )
{
cout << "len == 0 !!!" << endl;
return;
}
int level = log10(len)/log10(2)+1;//得到樹的高度
cout << "level = " << level << endl;
printSpace(level*2);
cout << A[0];
printSpace(level*2);
level--;
cout << endl;
int index = 1;
for(int i = 0;i < level; ++i )
{
int count = pow(2,i);
while(count--)
{
printSpace((level-i)*2);
if(index < len) cout << A[index++];
printSpace((level-i)*2);
if(index < len)cout << A[index++];
printSpace(count);
}
cout << endl;
}
}
PS:簡陋的程式,只能用於畫完全二叉樹。