1. 基本概念
堆,分為大頂堆(大堆)和小頂堆(小堆),是順序存儲的完全二叉樹,並且滿足以下特性之一:
(1) 任意非終端結點關鍵字不小於左右子結點(大堆)
ki >= k2i+1並且ki>=k2i+2 其中,0 <= i <= (n-1)/2,n是數組元素個數
(2) 任意非終端結點關鍵字不大於左右子結點(小堆)
ki <= k2i+1並且ki<=k2i+2 其中,0 <= i <= (n-1)/2,n是數組元素個數
調整(也有叫篩選):
從當前結點(要求是非終端結點)開始,
對於大堆,要求當前結點關鍵字不小於子結點,如不符合,則將最大的子結點與當前結點交換。循環迭代交換后的子樹,確保所有子樹都符合大堆特性。
小堆調整過程類似。
2. 基本思想
堆排序就是利用構建堆和輸出堆頂元素的過程,不斷對堆進行調整以保證當前結點及其孩子結點滿足堆特性,從而達到對初始數組元素進行排序的目的。
大堆通常對應升序序列,小堆通常對應降序排列。
核心步驟:
1) 構建堆(大堆/小堆)
從最后一個非終端結點開始,向前進行調整,保證當前結點及其子樹符合堆特性;
2) 輸出有序序列
交換堆頂與末尾葉子結點,堆頂輸出到數組的有序序列末尾,而不參與堆的調整。從交換后的堆頂開始調整,以確保當前結點及其子樹符合堆特性。
3. 實例
下面舉個例子,利用小堆進行降序排列。
初始序列 |
49 |
38 |
65 |
97 |
76 |
13 |
27 |
49‘ |
位置 |
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
說明:
|
3.1. 構建堆
1) 初始序列對應初始堆
從最后一個非葉子結點開始,向前進行調整,確保符合特性
最后一個非葉子結點位置:(n-1) / 2 = 3, n=8
總共調整次數:(n-1)/2 +1 = 4
第1次調整:選擇最后一個非葉子結點元素為97(位置3)為當前父結點,與其子結點進行比較,選擇最小的結點作為當前父結點。
第1次調整后序列 |
49 |
38 |
65 |
49’ |
76 |
13 |
27 |
97 |
位置 |
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
第2次調整:選擇上一次結點的前一個結點65(位置2)為當前結點進行調整。
第2次調整后序列 |
49 |
38 |
13 |
49’ |
76 |
65 |
27 |
97 |
位置 |
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
第3次調整:選擇上一次結點的前一個結點38(位置1)為當前結點進行調整。
第3次調整后序列 |
49 |
38 |
13 |
49’ |
76 |
65 |
27 |
97 |
位置 |
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
第4次調整:選擇上一次結點的前一個結點49(位置0)為當前結點進行調整。
第4次調整后序列 |
13 |
38 |
27 |
49’ |
76 |
65 |
49 |
97 |
位置 |
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
3.2. 輸出堆頂元素
將已經構建好的小堆,輸出堆頂元素,和末尾元素交換,相當於堆頂移動到數組末尾形成有序序列,未排序元素移動到堆頂。從新的堆頂開始進行調整,直到堆重新符合小堆特性。
交換堆頂和末尾(堆的末尾,不包括已經排好序的部分),並將交換后的堆末尾作為有序序列的一部分,而不再屬於堆。
一次交換后,發現97新的位置比子結點大,需要繼續調整。
..
這樣,不斷輸出所有堆頂到數組末尾,最終可以得到
有序序列 |
97 |
76 |
65 |
49 |
49’ |
38 |
27 |
13 |
位置 |
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
4. 實現代碼
4.1. 核心算法代碼
1 /** 2 * 小堆 <=> 升序排列 3 * 從最后一個非葉子結點開始, 向前進行調整 4 * @param a 待排序序列(數組) 5 * @param n 待排序元素個數 6 */ 7 void HeapSort(int a[], int n) 8 { 9 // S1 建堆 10 // 從最后一個非葉子結點開始, 向前進行調整 11 for(int i = LocOfLastNonLeaf(a, n); i >= 0; i --) 12 {// LocOfLastNonLeaf : (n-1) / 2 13 HeapAdjust(a, i, n); 14 } 15 16 // S2 輸出並調整 17 for(int j = n-1; j > 0; j --) 18 {// 判斷條件不用加"=", 因為j=0時等價於數組只有一個元素, 即只有一個根節點, 而無子樹 19 Swap(a[0], a[j]); // 形參為同名參數, 直接交換a[0]和a[j] 20 HeapAdjust(a, 0, j); 21 } 22 }
1 /** 2 * 篩選位置i, 調整堆 3 * @param a 待排序序列(數組) 4 * @param i 篩選位置 5 * @param len 數組元素個數 6 */ 7 void HeapAdjust(int a[], int i, int n) 8 { 9 if(i > n / 2 - 1) 10 {// 葉子結點, 無子樹 11 return; 12 } 13 14 // 檢查結點i是否符合小堆特性, 如果不符合, 需要與最小子結點交換 15 for(int k = 2*i +1; k < n; k = 2*k + 1) 16 { 17 // 判斷右子樹是否比左子樹更小 18 if(k+1 < n && a[k+1] < a[k]) 19 { 20 k ++; // 更新最小子結點 21 } 22 23 if(a[i] > a[k]) 24 { 25 Swap(a[i], a[k]); // 與最小的子結點交換 26 i = k; // 將左子結點設為當前結點 27 } 28 else 29 {// 符合小堆特性 30 break; 31 } 32 } 33 }
4.2. 調用
1 int main() 2 { 3 int a[] = { 4 // 10,2,5,9,5,55,21,33,15 5 49, 38, 65, 97, 76, 13, 27, 49 6 }; 7 int a_len = sizeof(a) / sizeof(a[0]); 8 9 printf("原始序列: "); 10 PrintArrary(a, a_len); // 打印數組 11 12 HeapSort(a, a_len); // 堆排序 13 14 printf("小堆排序后序列(降序): "); 15 PrintArrary(a, a_len); // 打印數組 16 17 return 0; 18 }
4.3. 測試結果