首先看一下堆的定義:
對於n個元素的序列{k1,k2,k3,……,kn},當且僅當滿足下列關系時,稱之為堆:
K(i) <= K(2*i) && K(i) <= K(2*i+1) 此時的堆為小頂堆
K(i) >= K(2*i) && K(i) >= K(2*i+1) 此時的堆為大頂堆
(i = 1,2,……,n/2(下取整))
注意:堆得存儲是用一維數組來存儲的。
若將堆對應的序列看成是一個完全二叉樹,則堆得含義表明:
完全二叉樹中所有非終端結點的值均不大於(或不小於)其左右孩子結點的值。
因此,若序列{K1,K2,……,Kn} 是大頂堆,則堆頂元素必為序列中n個元素的最大值;反之,若序列是小頂堆,則堆頂元素必為序列中n個元素的最小值。
堆排序就是利用的這個性質。
堆排序的過程如下:
假設要從小到大排序,我們構建一個大頂堆,則堆頂元素是最大值。將堆頂元素和最后一個元素互換,則最后一個元素變成了n個元素中的最大值。之后再將剩下的 n-1 個元素調整成為大頂堆,將堆頂元素和第n-1 個元素互換,則第n-1 個元素變成了n個元素中的次大值……循環這個過程,不斷調整堆,最后得到一個有序的序列。
在上面堆排序的過程中,有兩個問題需要解決:
(1)如何將一個初始的序列構建成一個大頂堆?
(2)再得到最大元素后,剩下的n-1個元素如何再次調整成為一個大頂堆?
實際上,初始序列構建大頂堆也是一個不斷調整堆得過程。因此,只要解決第二個問題就可以。
下圖是一個大頂堆:

當把堆頂元素20和最后一個元素互換之后,最后一個元素變成了序列中的最大值。如下圖:

但是,此時堆頂元素違反了大頂堆的性質,堆頂元素的左右孩子仍舊滿足大頂堆的性質。因此,此時需要對堆進行調整。因為左子樹的值大於右子樹的值,所以將3和17互換,如下圖:

此時,左子樹又違反了大頂堆得性質,所以需要調整左子樹,如下圖:

至此,一次調整完畢,堆頂元素成為了次大元素。
實際上,調整堆就是這樣一個不斷篩選比較的過程,不斷的和左右子樹比較,一直到不需要交換為止。
將一個無序序列構建成一個大頂堆的過程就是一個反復篩選的過程。將此序列看成是一個完全二叉樹,則最后一個非葉子節點是第 n/2(下取整)個元素,因此,篩選只需從第 n/2(下取整)個元素開始。
假設有序列:{49,38,65,97,76,13,27},初始二叉樹是:

從第3個元素,也就是65開始調整堆,65大於左右子樹的值,因此不需要調整。
然后是第2個元素,也就是從38開始調整堆,38和左右子樹比較,將97和38互換,調整后如下圖:

然后是第1個元素,也就是從49開始調整堆,49和左右子樹比較,將97和49互換,互換之后,因為49破壞了左子樹大頂堆的性質,因此需要繼續調整,將49和左右子樹比較,然后將49和76互換,調整過程如下圖:


至此,將一個無序的序列調整成為了一個大頂堆。
同理,堆排序也分為兩個過程:(1)將初始化序列調整成為一個大頂堆(2)用最后一個元素和堆頂元素交換,然后不斷調整剩下的元素成為一個新的大頂堆。
代碼如下:
const int N = 8;
int num[N] = {-1,49,38,65,97,76,13,27}; //從第一個元素開始存儲
//調整堆的函數
void heapAdjust(int pos,int total){
int temp = num[pos];
for(int j = 2 * pos; j <= total; j *= 2){
if(j < total){ //說明還有右子樹
if(num[j] < num[j + 1]){ //篩選出左右子樹中較大的值
j += 1;
}
}
if(temp >= num[j]) //不需要再繼續向下調整了
break;
num[pos] = num[j];
pos = j;
}
num[pos] = temp;
}
void heapSort(){
//首先將數組構建成一個大頂堆
for(int i = (N-1) / 2;i >=1;--i){
heapAdjust(i,N - 1);
}
//開始堆排序
for(int i = N-1; i > 1; --i){
int temp = num[i]; //交換第一個元素和最后一個元素
num[i] = num[1];
num[1] = temp;
heapAdjust(1,i - 1); //交換完之后,重新調整堆
}
}
