本章開始介紹了堆的基本概念,然后引入最大堆和最小堆的概念。全章采用最大堆來介紹堆的操作,兩個重要的操作是調整最大堆和創建最大堆,接着着兩個操作引進了堆排序,最后介紹了采用堆實現優先級隊列。
1、堆
堆給人的感覺是一個二叉樹,但是其本質是一種數組對象,因為對堆進行操作的時候將堆視為一顆完全二叉樹,樹種每個節點與數組中的存放該節點值的那個元素對應。所以堆又稱為二叉堆,堆與完全二叉樹的對應關系如下圖所示:
通常給定節點i,可以根據其在數組中的位置求出該節點的父親節點、左右孩子節點,這三個過程一般采用宏或者內聯函數實現。書上介紹的時候,數組的下標是從1開始的,所有可到:PARENT(i)=i/2 LEFT(i) = 2*i RIGHT(i) = 2*i+1。
根據節點數值滿足的條件,可以將分為最大堆和最小堆。最大堆的特性是:除了根節點以外的每個節點i,有A[PARENT(i)] >= A[i],最小堆的特性是:除了根節點以外的每個節點i,有A[PARENT(i)] >=A[i]。
把堆看成一個棵樹,有如下的特性:
(1)含有n個元素的堆的高度是lgn。
(2)當用數組表示存儲了n個元素的堆時,葉子節點的下標是n/2+1,n/2+2,……,n。
(3)在最大堆中,最大元素該子樹的根上;在最小堆中,最小元素在該子樹的根上。
2、保持堆的性質
堆個關鍵操作過程是如何保持堆的特有性質,給定一個節點i,要保證以i為根的子樹滿足堆性質。書中以最大堆作為例子進行講解,並給出了遞歸形式的保持最大堆性的操作過程MAX-HEAPIFY。先從看一個例子,操作過程如下圖所示:
從圖中可以看出,在節點i=2時,不滿足最大堆的要求,需要進行調整,選擇節點2的左右孩子中最大一個進行交換,然后檢查交換后的節點i=4是否滿足最大堆的要求,從圖看出不滿足,接着進行調整,直到沒有交換為止。書中給出了遞歸形式的為代碼,我用C語言實現如下所示:
1 void adjust_max_heap_recursive(int *datas,int length,int i) 2 { 3 int left,right,largest; 4 int temp; 5 left = LEFT(i); //left child 6 right = RIGHT(i); //right child 7 //find the largest value among left and rihgt and i. 8 if(left<=length && datas[left] > datas[i]) 9 largest = left; 10 else 11 largest = i; 12 if(right <= length && datas[right] > datas[largest]) 13 largest = right; 14 //exchange i and largest 15 if(largest != i) 16 { 17 temp = datas[i]; 18 datas[i] = datas[largest]; 19 datas[largest] = temp; 20 //recursive call the function,adjust from largest 21 adjust_max_heap(datas,length,largest); 22 } 23 }
課后習題要求給出其非遞歸的形式,我想了半天,才搞出來,領悟能力有限啊。非遞歸就要考慮要循環進行實現,需要考慮的是循環結束條件是什么。對一個給定的節點i,要對其進行調整使其滿足最大堆的性質。總的思想是先找出節點i的左右孩子節點,然后從三者中找到最大的節點,如果找到的最大節點就是i,說明i節點滿足堆的性質,此時循環就結束了。如果找到的最大節點不是節點i,那么這個時候就要將最大的節點(設為largest)與節點i進行交換,然后從largest節點開始循環進行調整,直到滿足條件為止。給出非遞歸的調整堆程序如下:
1 void adjust_max_heap(int *datas,int length,int i) 2 { 3 int left,right,largest; 4 int temp; 5 while(1) 6 { 7 left = LEFT(i); //left child 8 right = RIGHT(i); //right child 9 //find the largest value among left and rihgt and i. 10 if(left <= length && datas[left] > datas[i]) 11 largest = left; 12 else 13 largest = i; 14 if(right <= length && datas[right] > datas[largest]) 15 largest = right; 16 //exchange i and largest 17 if(largest != i) 18 { 19 temp = datas[i]; 20 datas[i] = datas[largest]; 21 datas[largest] = temp; 22 i = largest; 23 continue; 24 } 25 else 26 break; 27 } 28 }
3、建堆
建立最大堆的過程是自底向上地調用最大堆調整程序將一個數組A[1.....N]變成一個最大堆。將數組視為一顆完全二叉樹,從其最后一個非葉子節點(n/2)開始調整。調整過程如下圖所示:
書中給出了創建堆的為代碼,我用C語言實現如下:
1 void build_max_heap(int *datas,int length) 2 { 3 int i; 4 //build max heap from the last parent node 5 for(i=length/2;i>0;i--) 6 adjust_max_heap(datas,length,i); 7 }
4、堆排序算法
堆排序算法過程為:先調用創建堆函數將輸入數組A[1...n]造成一個最大堆,使得最大的值存放在數組第一個位置A[1],然后用數組最后一個位置元素與第一個位置進行交換,並將堆的大小減少1,並調用最大堆調整函數從第一個位置調整最大堆。給出堆數組A={4,1,3,16,9,10,14,8,7}進行堆排序簡單的過程如下:
(1)創建最大堆,數組第一個元素最大,執行后結果下圖:
(2)進行循環,從length(a)到2,並不斷的調整最大堆,給出一個簡單過程如下:
書中給出了對排序為代碼,我用C語言實現如下所示:
1 void heap_sort(int *datas,int length) 2 { 3 int i,temp; 4 //bulid max heap 5 build_max_heap(datas,length); 6 i=length; 7 //exchange the first value to the last unitl i=1 8 while(i>1) 9 { 10 temp = datas[i]; 11 datas[i] = datas[1]; 12 datas[1] =temp; 13 i--; 14 //adjust max heap,make sure the fisrt value is the largest 15 adjust_max_heap(datas,i,1); 16 } 17 }
結合上面的調整堆和創建堆 的過程,寫個簡單測試程序連續堆排序,程序如下所示:

1 #include <stdio.h> 2 #include <stdlib.h> 3 4 //array's index begins 1,not 0 5 6 #define PARENT(i) (i/2) 7 #define LEFT(i) (i*2) 8 #define RIGHT(i) (i*2+1) 9 #define NOTNUSEDATA -65536 10 11 void adjust_max_heap(int *datas,int length,int i); 12 void adjust_max_heap_recursive(int *datas,int length,int i); 13 void build_max_heap(int *datas,int length); 14 void heap_sort(int *datas,int length); 15 16 int main() 17 { 18 int i; 19 //array's index begin 1 20 int datas[11] = {NOTNUSEDATA,5,3,17,10,84,19,6,22,9,35}; 21 heap_sort(datas,10); 22 for(i=1;i<11;++i) 23 printf("%d ",datas[i]); 24 printf("\n"); 25 exit(0); 26 } 27 28 void adjust_max_heap_recursive(int *datas,int length,int i) 29 { 30 int left,right,largest; 31 int temp; 32 left = LEFT(i); //left child 33 right = RIGHT(i); //right child 34 //find the largest value among left and rihgt and i. 35 if(left<=length && datas[left] > datas[i]) 36 largest = left; 37 else 38 largest = i; 39 if(right <= length && datas[right] > datas[largest]) 40 largest = right; 41 //exchange i and largest 42 if(largest != i) 43 { 44 temp = datas[i]; 45 datas[i] = datas[largest]; 46 datas[largest] = temp; 47 //recursive call the function,adjust from largest 48 adjust_max_heap(datas,length,largest); 49 } 50 } 51 void adjust_max_heap(int *datas,int length,int i) 52 { 53 int left,right,largest; 54 int temp; 55 while(1) 56 { 57 left = LEFT(i); //left child 58 right = RIGHT(i); //right child 59 //find the largest value among left and rihgt and i. 60 if(left <= length && datas[left] > datas[i]) 61 largest = left; 62 else 63 largest = i; 64 if(right <= length && datas[right] > datas[largest]) 65 largest = right; 66 //exchange i and largest 67 if(largest != i) 68 { 69 temp = datas[i]; 70 datas[i] = datas[largest]; 71 datas[largest] = temp; 72 i = largest; 73 continue; 74 } 75 else 76 break; 77 } 78 } 79 void build_max_heap(int *datas,int length) 80 { 81 int i; 82 //build max heap from the last parent node 83 for(i=length/2;i>0;i--) 84 adjust_max_heap(datas,length,i); 85 } 86 void heap_sort(int *datas,int length) 87 { 88 int i,temp; 89 //bulid max heap 90 build_max_heap(datas,length); 91 i=length; 92 //exchange the first value to the last unitl i=1 93 while(i>1) 94 { 95 temp = datas[i]; 96 datas[i] = datas[1]; 97 datas[1] =temp; 98 i--; 99 //adjust max heap,make sure the fisrt value is the largest 100 adjust_max_heap(datas,i,1); 101 } 102 }
程序測試結果如下所示:
從結果可以看出按照最大堆進行堆排序最終使得結果是從小到大排序(非遞減的)。
堆排序算法時間復雜度:調整堆過程滿足遞歸式T(n)<=T(2n/3)+θ(1),有master定義可以知道T(n) = O(lgn),堆排序過程中執行一個循環,調用最大堆調整函數,總的時間復雜度為O(nlgn)。
5、問題
(1)在創建最大堆的過程中,為什么從最后一個非葉子節點(n/2)開始到第一個非葉子(1)結束,而不是從第一個非葉子節點(1)到最后一個非葉子節點(n/2)結束呢?
我的想法是:如果是從第一個非葉子節點開始創建堆,有可能導致創建的堆不滿足堆的性質,使得第一個元素不是最大的。這樣做只是使得該節點的和其左右孩子節點滿足堆性質,不能確保整個樹滿足堆的性質。如果最大的節點在葉子節點,那么將可能不會出現在根節點中。例如下面的例子:
從圖中可以看出,從第一個非葉子節點開始創建最大堆,最后得到的結果並不是最大堆。而從最后一個非葉子節點開始創建堆時候,能夠保證該節點的子樹都滿足堆的性質,從而自底向上進行調整堆,最終使得滿足最大堆的性質。