堆排序
之前的隨筆寫了棧(順序棧、鏈式棧)、隊列(循環隊列、鏈式隊列)、鏈表、二叉樹,這次隨筆來寫堆
1、什么是堆?
堆是一種非線性結構,(本篇隨筆主要分析堆的數組實現)可以把堆看作一個數組,也可以被看作一個完全二叉樹,通俗來講堆其實就是利用完全二叉樹的結構來維護的一維數組
按照堆的特點可以把堆分為大頂堆和小頂堆
大頂堆:每個結點的值都大於或等於其左右孩子結點的值
小頂堆:每個結點的值都小於或等於其左右孩子結點的值
(堆的這種特性非常的有用,堆常常被當做優先隊列使用,因為可以快速的訪問到“最重要”的元素)
2、堆的特點(數組實現)
(圖片來源:https://www.cnblogs.com/chengxiao/p/6129630.html)
我們對堆中的結點按層進行編號,將這種邏輯結構映射到數組中就是下面這個樣子
(圖片來源:https://www.cnblogs.com/chengxiao/p/6129630.html)
我們用簡單的公式來描述一下堆的定義就是:(讀者可以對照上圖的數組來理解下面兩個公式)
大頂堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]
小頂堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]
3、堆和普通樹的區別
內存占用:
普通樹占用的內存空間比它們存儲的數據要多。你必須為節點對象以及左/右子節點指針分配額外的內存。堆僅僅使用數組,且不使用指針
(可以使用普通樹來模擬堆,但空間浪費比較大,不太建議這么做)
搜索:
4、堆排序的過程
先了解下堆排序的基本思想:
將待排序序列構造成一個大頂堆,此時,整個序列的最大值就是堆頂的根節點。將其與末尾元素進行交換,此時末尾就為最大值。然后將剩余n-1個元素重新構造成一個堆,這樣會得到n個元素的次小值,
如此反復執行,便能得到一個有序序列了,建立最大堆時是從最后一個非葉子節點開始從下往上調整的(這句話可能不好太理解),下面會舉一個例子來理解堆排序的基本思想
給一個無序序列如下
int a[6] = {7, 3, 8, 5, 1, 2};
現在可以根據數組將完全二叉樹還原出來
好了,現在我們要做的事情就是要把7,3,8,5,1,2變成一個有序的序列,如果想要升序就是1,2,3,5,7,8 如果想要降序就是8,7,5,3,2,1 ,這兩種就是我們要的最終結果,然后我們就可以根據我們想要的結果來選擇
適合類型的堆來進行排序
升序----使用大頂堆
降序----使用小頂堆
5、為什么升序要用大頂堆呢
上面提到過大頂堆的特點:每個結點的值都大於或等於其左右孩子結點的值,我們把大頂堆構建完畢后根節點的值一定是最大的,然后把根節點的和最后一個元素(也可以說最后一個節點)交換位置,那么末尾元素此時就是最大元素了(理解這點很重要)
知道了堆排序的原理下面就可以來操作了,在進行操作前先理清一下步驟
(假設我們想要升序的排列)
第一步:先n個元素的無序序列,構建成大頂堆
第二步:將根節點與最后一個元素交換位置,(將最大元素"沉"到數組末端)
第三步:交換過后可能不再滿足大頂堆的條件,所以需要將剩下的n-1個元素重新構建成大頂堆
第四步:重復第二步、第三步直到整個數組排序完成
6、圖解交換過程(得到升序序列,使用大頂堆來調整)
這里以int a[6] = {7, 3, 8, 5, 1, 2}為例子
先要找到最后一個非葉子節點,數組的長度為6,那么最后一個非葉子節點就是:長度/2-1,也就是6/2-1=2,然后下一步就是比較該節點值和它的子樹值,如果該節點小於其左\右子樹的值就交換(意思就是將最大的值放到該節點)
8只有一個左子樹,左子樹的值為2,8>2不需要調整
下一步,繼續找到下一個非葉子節點(其實就是當前坐標-1就行了),該節點的值為3小於其左子樹的值,交換值,交換后該節點值為5,大於其右子樹的值,不需要交換
下一步,繼續找到下一個非葉子節點,該節點的值為7,大於其左子樹的值,不需要交換,再看右子樹,該節點的值小於右子樹的值,需要交換值
下一步,檢查調整后的子樹,是否滿足大頂堆性質,如果不滿足則繼續調整(這里因為只將右子樹的值與根節點互換,只需要檢查右子樹是否滿足,而7>2剛好滿足大頂堆的性質,就不需要調整了,
如果運氣不好整個樹的根節點的值是1,那么就還需要調整右子樹)
到這里大頂堆的構建就算完成了,然后下一步交換根節點(8)與最后一個元素(2)交換位置(將最大元素"沉"到數組末端),此時最大的元素就歸位了,然后對剩下的5個元素重復上面的操作
(這里用粉紅色來表示已經歸位的元素)
剩下只有5個元素,最后一個非葉子節點是5/2-1=1,該節點的值(5)大於左子樹的值(3)也大於右子樹的值(1),滿足大頂堆性質不需要交換
找到下一個非葉子節點,該節點的值(2)小於左子樹的值(5),交換值,交換后左子樹不再滿足大頂堆的性質再調整左子樹,左子樹滿足要求后再返回去看根節點,根節點的值(5)小於右子樹的值(7),再次交換值
得到新的大頂堆,如下圖,再把根節點的值(7)與當前數組最后一個元素值(1)交換,再重構大頂堆->交換值->重構大頂堆->交換值····,直到整個數組都變成有序序列
最后得到的升序序列如下圖
7、堆排序的代碼實現
上面說了一大堆來詳細說明堆排序的操作步驟,下面開始就開始來碼代碼了
筆者將堆排序的過程分成了兩個子函數
void Swap(int *heap, int len); /* 交換根節點和數組末尾元素的值 */
void BuildMaxHeap(int *heap, int len);/* 構建大頂堆 */
先來實現構建大堆的部分:
1 /* Function: 構建大頂堆 */
2 void BuildMaxHeap(int *heap, int len) 3 { 4 int i; 5 int temp; 6
7 for (i = len/2-1; i >= 0; i--) 8 { 9 if ((2*i+1) < len && heap[i] < heap[2*i+1]) /* 根節點小於左子樹 */
10 { 11 temp = heap[i]; 12 heap[i] = heap[2*i+1]; 13 heap[2*i+1] = temp; 14 /* 檢查交換后的左子樹是否滿足大頂堆性質 如果不滿足 則重新調整子樹結構 */
15 if ((2*(2*i+1)+1 < len && heap[2*i+1] < heap[2*(2*i+1)+1]) || (2*(2*i+1)+2 < len && heap[2*i+1] < heap[2*(2*i+1)+2])) 16 { 17 BuildMaxHeap(heap, len); 18 } 19 } 20 if ((2*i+2) < len && heap[i] < heap[2*i+2]) /* 根節點小於右子樹 */
21 { 22 temp = heap[i]; 23 heap[i] = heap[2*i+2]; 24 heap[2*i+2] = temp; 25 /* 檢查交換后的右子樹是否滿足大頂堆性質 如果不滿足 則重新調整子樹結構 */
26 if ((2*(2*i+2)+1 < len && heap[2*i+2] < heap[2*(2*i+2)+1]) || (2*(2*i+2)+2 < len && heap[2*i+2] < heap[2*(2*i+2)+2])) 27 { 28 BuildMaxHeap(heap, len); 29 } 30 } 31 } 32 }
上述代碼中不易於理解的可能就是下面這條if判斷語句
/* 檢查交換后的左子樹是否滿足大頂堆性質 如果不滿足 則重新調整子樹結構 */
if ((2*(2*i+1)+1 < len && heap[2*i+1] < heap[2*(2*i+1)+1]) || (2*(2*i+1)+2 < len && heap[2*i+1] < heap[2*(2*i+1)+2])) { BuildMaxHeap(heap, len); }
把if里面的條件分來開看2*(2*i+1)+1 < len的作用是判斷該左子樹有沒有左子樹(可能有點繞),heap[2*i+1] < heap[2*(2*i+1)+1]就是判斷左子樹的左子樹的值是否大於左子樹,如果是,那么就意味着交換值
過后左子樹大頂堆的性質被破環了,需要重構該左子樹
下面來實現交換部分
1 /* Function: 交換交換根節點和數組末尾元素的值*/
2 void Swap(int *heap, int len) 3 { 4 int temp; 5
6 temp = heap[0]; 7 heap[0] = heap[len-1]; 8 heap[len-1] = temp; 9 }
然后來考慮下主函數部分,因為是int a[6] = {7, 3, 8, 5, 1, 2}長度為6,需要構建大頂堆,交換值6次才能得到有序序列,由此可以確定主函數的for循環為,for (i = len; i > 0; i--)
1 int main() 2 { 3 int a[6] = {7, 3, 8, 5, 1, 2}; 4 int len = 6; /* 數組長度 */
5 int i; 6
7 for (i = len; i > 0; i--) 8 { 9 BuildMaxHeap(a, i); 10 Swap(a, i); 11 } 12 for (i = 0; i < len; i++) 13 { 14 printf("%d ", a[i]); 15 } 16
17 return 0; 18 }
下面附上堆排序完整代碼:

1 #include <stdio.h> 2 3 void Swap(int *heap, int len); /* 交換根節點和數組末尾元素的值 */ 4 void BuildMaxHeap(int *heap, int len);/* 構建大頂堆 */ 5 6 int main() 7 { 8 int a[6] = {7, 3, 8, 5, 1, 2}; 9 int len = 6; /* 數組長度 */ 10 int i; 11 12 for (i = len; i > 0; i--) 13 { 14 BuildMaxHeap(a, i); 15 Swap(a, i); 16 } 17 for (i = 0; i < len; i++) 18 { 19 printf("%d ", a[i]); 20 } 21 22 return 0; 23 } 24 /* Function: 構建大頂堆 */ 25 void BuildMaxHeap(int *heap, int len) 26 { 27 int i; 28 int temp; 29 30 for (i = len/2-1; i >= 0; i--) 31 { 32 if ((2*i+1) < len && heap[i] < heap[2*i+1]) /* 根節點大於左子樹 */ 33 { 34 temp = heap[i]; 35 heap[i] = heap[2*i+1]; 36 heap[2*i+1] = temp; 37 /* 檢查交換后的左子樹是否滿足大頂堆性質 如果不滿足 則重新調整子樹結構 */ 38 if ((2*(2*i+1)+1 < len && heap[2*i+1] < heap[2*(2*i+1)+1]) || (2*(2*i+1)+2 < len && heap[2*i+1] < heap[2*(2*i+1)+2])) 39 { 40 BuildMaxHeap(heap, len); 41 } 42 } 43 if ((2*i+2) < len && heap[i] < heap[2*i+2]) /* 根節點大於右子樹 */ 44 { 45 temp = heap[i]; 46 heap[i] = heap[2*i+2]; 47 heap[2*i+2] = temp; 48 /* 檢查交換后的右子樹是否滿足大頂堆性質 如果不滿足 則重新調整子樹結構 */ 49 if ((2*(2*i+2)+1 < len && heap[2*i+2] < heap[2*(2*i+2)+1]) || (2*(2*i+2)+2 < len && heap[2*i+2] < heap[2*(2*i+2)+2])) 50 { 51 BuildMaxHeap(heap, len); 52 } 53 } 54 } 55 } 56 57 /* Function: 交換交換根節點和數組末尾元素的值*/ 58 void Swap(int *heap, int len) 59 { 60 int temp; 61 62 temp = heap[0]; 63 heap[0] = heap[len-1]; 64 heap[len-1] = temp; 65 }
運行結果:
雖然STL模板庫給我們提供了兩種簡單方便堆操作的方式,很多高級語言的也有很多常見數據結構的封裝,筆者還是建議需要學習數據結構相關的內容,至少要了解不同的數據結構
避免在使用高級語言的封裝好的數據結構時出現只會用不理解的尷尬情況····