1. 程式人生 > >堆排序演算法設計與分析

堆排序演算法設計與分析

堆排序(HeapSort)是指利用堆積樹(堆)這種資料結構所設計的一種排序演算法,它是選擇排序的一種。堆分為大根堆和小根堆,是完全二叉樹。大根堆要求父結點的值大於或等於子結點的值,小根堆相反。根據大根堆的性質,我們可以知道最大值一定在堆頂,即根結點,利用這一點我們可以將陣列建成大根堆。這裡我以大根堆為例,小根堆類似。

大根堆的主要思想是:先將陣列建為大根堆(建堆過程後面介紹),此為初始堆。此時我們知道堆頂元素為最大值,即陣列第一個元素為最大值,我們將陣列第一個元素和最後一個交換,此時陣列最後一個元素為最大值,然後我們對第一個元素在去除最後一個元素的堆中進行更新,保證第一個元素在當前堆中為最大值,再次與此時堆中最後一個元素交換,再次去除最後一個元素更新堆,依次類推,直到堆中只剩一個元素。

大根堆排序演算法的基本操作:
①建堆,建堆是不斷調整堆的過程,從(len/2-1)處,即最後一個非葉結點開始調整,一直到第一個節點,此處len是堆中元素的個數。建堆的過程是線性的過程,從len/2-1到0處一直呼叫調整堆的過程,相當於o(h1)+o(h2)…+o(h(len/2-1)) 其中h表示節點的深度,len/2-1表示節點的個數,這是一個求和的過程,結果是線性的O(n)。
②調整堆:調整堆在構建堆的過程中會用到,而且在堆排序過程中也會用到。利用的思想是比較節點i和它的孩子節點left(i),right(i),選出三者最大(或者最小)者,如果最大(小)值不是節點i而是它的一個孩子節點,那邊互動節點i和該節點,然後再呼叫調整堆過程,這是一個遞迴的過程。調整堆的過程時間複雜度與堆的深度有關係,是lgn的操作,因為是沿著深度方向進行調整的。
③堆排序:堆排序是利用上面的兩個過程來進行的。首先是根據元素構建堆。然後將堆的根節點取出(一般是與最後一個節點進行交換),將前面len-1個節點繼續進行堆調整的過程,然後再將根節點取出,這樣一直到所有節點都取出。堆排序過程的時間複雜度是O(nlgn)。因為建堆的時間複雜度是O(n)(呼叫一次);調整堆的時間複雜度是lgn,呼叫了n-1次,所以堆排序的時間複雜度是O(nlgn)。

可以參考下圖理解:






下面是程式碼:

<span style="font-size:14px;">//本函式功能是:根據陣列arr構建大根堆
//arr是待調整的堆陣列,index是待調整的陣列元素的位置,length是陣列的長度
void HeapAdjust(int* arr, int index, int length)
{
	int nChild;//子結點
	int temp;
	for (; 2 * index + 1 < length; index = nChild)
	{
		//子結點的位置=2*(父結點位置)+1
		nChild = 2 * index + 1;
		//得到子節點中較大的結點
		if (nChild<length - 1 && arr[nChild + 1]>arr[nChild])
			nChild++;
		//如果較大的子結點大於父結點那麼把較大的子結點往上移動,與父結點交換
		if (arr[index] < arr[nChild])
		{
			temp = arr[index];
			arr[index] = arr[nChild];
			arr[nChild] = temp;
		}
		else
			break;//如果子結點小於父結點則退出迴圈
	}
}

//堆排序
void HeapSort(int* arr, int length)
{
	int i;
	//構建大根堆,構建完後第一個元素是序列的最大元素
	for (i = length / 2 - 1; i >= 0; --i)//(length/2-1)是最後一個非葉結點
		HeapAdjust(arr, i, length);
	//從最後一個元素開始對序列進行調整,不斷縮小範圍直到第一個元素
	for (i = length - 1; i > 0; --i)
	{
		//把第一個元素(根元素,也就是最大的元素)和當前最後一個交換
		arr[i] = arr[0] ^ arr[i];
		arr[0] = arr[0] ^ arr[i];
		arr[i] = arr[0] ^ arr[i];
		//不斷縮小調整堆的範圍,每一次調整完畢後保證第一個元素是當前序列的最大值
		HeapAdjust(arr, 0, i);
	}
}</span>
堆排序是就地排序,輔助空間為o(1)。它是不穩定的排序演算法。

從平均時間效能而言,快速排序最佳,其所需時間最省,但快速排序在最壞情況下的時間效能不如堆排序和歸併排序。而後兩者比較的結果是,在n較大時,歸併排序所需時間較堆排序省,但它所需的輔助儲存量最多。

我測試了一個一億的陣列,快速排序用了28.792393秒,而堆排序用了102.245783秒。

通俗點講,堆排序就是分為建堆和更新堆兩個步驟。
 堆排序利用了大根堆(或小根堆)堆頂記錄的關鍵字最大(或最小)這一特徵, 使得在當前無序區中選取最大(或最小)關鍵字的記錄變得簡單。   
1)用大根堆排序的基本思想    ① 先將初始檔案R[1..n]建成一個大根堆,此堆為初始的無序區    ②
再將關鍵字最大的記錄R[1](即堆頂)和無序區的最後一個 記錄R[n]交換,由此得到新的無序區R[1..n-1]和有序區R[n],
且滿足R[1..n-1].keys≤R[n].key    ③由於交換後新的根R[1]可能違反堆性質,故應將當前無序區R[1..n-1]調整為堆。
然後再次將R[1..n-1]中關鍵字最大的記錄R[1]和該區間的最後一個記錄R[n-1]交換,
由此得到新的無序區R[1..n-2]和有序區R[n-1..n],
且仍滿足關係R[1..n-2].keys≤R[n-1..n].keys,同樣要將R[1..n-2]調整為堆。   直到無序區只有一個元素為止。   
2)大根堆排序演算法的基本操作:    ① 初始化操作:將R[1..n]構造為初始堆;    ②
每一趟排序的基本操作:將當前無序區的堆頂記錄R[1]和該區間的最後一個記錄交換, 然後將新的無序區調整為堆(亦稱重建堆)。

更多排序及比較請看我的另一篇部落格"排序演算法及並行分析":http://blog.csdn.net/secyb/article/details/51319391