堆是具有下列性質的二叉樹:每個結點的值都大於或等於其zuo'左右結點的值稱爲大頂錐;或者每個結點的值都小於或等於其左右孩子的值,稱爲小頂錐。
堆排序就是利用堆進行排序的方法。他的基本思想是,將待排序的序列構造成一個大頂錐。此時整個序列的最大值就是堆頂的結點。將他移走(其實就是將與其堆數組的末尾元素交換,此時末尾元素的值就是最大值),然後將剩下的n - 1個序列的值chon重新構造成大頂錐,這樣就會得到序列中的次大值。如此反覆執行,便能得到一個有序序列了。
代碼如下:
void swap(SqList *L,int i,int j) //交換數據
{
int tmp = L->r[i];
L->r[i] = L->r[j];
L->r[j] = tmp;
}
void HeapAdjust(SqList *L,int s,int m) //使序列成爲一個大頂錐
{
int tmp,j;
tmp = L->r[s];
for(j = 2*s;j <= m;j*=2)
{
if(j < m && L->r[j] < L->r[j + 1])
j++;
if(tmp >= L->r[j])
break;
L->r[s] = L->r[j];
s = j;
}
L->r[s] = tmp;
}
void HeapSort(SqList *L) //堆排序
{
int i;
for(i = L->length/2;i > 0;i--)
HeapAdjust(L,i,L->length);
for(i = L->length;i > 1;i--)
{
swap(L,1,i);
HeapAdjust(L,1,i - 1);
}
}
先看堆排序函數:
void HeapSort(SqList *L)
{
int i;
for(i = L->length/2;i > 0;i--) //把L中的r構建成一個大頂錐
HeapAdjust(L,i,L->length);
for(i = L->length;i > 1;i--)
{
swap(L,1,i); //將錐頂記錄和當前未經排序子序列的最後一個記錄交換
HeapAdjust(L,1,i - 1); //將剩下的重新調整爲大頂錐
}
}
從代碼中可以看出,整個排序過程中分爲兩個for循環。第一個for循環要完成的就是將現在的待排序序列構成一個大頂錐。第二個循環要完成的就是逐步將每個最大值的根結點與末尾元素交換,並且在調整成爲大頂錐。
假設要排序的序列爲{50,10,90,30,70,40,80,60,90},則L.length = 9,第一個for循環,i 從[ 9/2] = 4 開始,爲什麼從4開始呢,因爲他們都是有孩子的結點。
再看堆調整函數:
void HeapAdjust(SqList *L,int s,int m)
{
int tmp,j;
tmp = L->r[s];
for(j = 2*s;j <= m;j*=2) //沿關鍵字較大的孩子結點向下篩選
{
if(j < m && L->r[j] < L->r[j + 1])
j++; //j爲關鍵字中較大的記錄的下標
if(tmp >= L->r[j])
break;
L->r[s] = L->r[j];
s = j;
}
L->r[s] = tmp; //插入
}
1.函數被第一次調用時,s = 4,m = 9,傳入的SqList 參數的值爲length = 9,r[10] = {0,50,10,90,30,70,40,80,60,20}.
2.第4行,將r[s] = r[4] = 30賦給tmp。
3.第5~13行,循環遍歷其結點的孩子,由於二叉樹的性質,j 由2*s開始,以j*=2遞增(當前結點序號是s,其左孩子序號一定是2s).
4.第7~8行,此時 j = 8,j < m 說明他不是最後一個結點,如果r[j] < r[j + 1],則說明左孩子小於右孩子,j++則說明現在的j是左右孩子最大值的下標。
5.第9~10行,tmp = 30,r[j] = 60,不滿足條件。
6.第11~12行,將60賦給r[4],並令s = j = 8。也就是說,當前算出,以30爲根節點的子二叉樹,當前最大值是60,在第8個位置。
7.再循環因爲j = 2*j = 16,跳出循環。
8.第14行,將tmp = 30賦值給r[s] = r[8],完成30與60的交換工作,本次調用完成,再次調用此函數仍以這樣的步驟。
接下來堆排序函數的第6~11行就是正式的排序過程,就是將堆頂與堆尾不斷進行交換的過程。
複雜度:
時間複雜度O爲nlogn。性能要好於冒泡,簡單排序,直接插入的n*n的時間複雜度。空間複雜度上由於記錄的比較和交換是跳躍式進行,因此堆排序是一種不穩定的排序。