1. 程式人生 > >排序演算法(三)-- 堆排序

排序演算法(三)-- 堆排序

堆排序

堆排序演算法結合了插入排序和歸併排序演算法的優點,和插入排序一樣,堆排序不需要額外申請空間。它是一種原地排序的演算法;和歸併排序一樣,堆排序的執行時間也是O(nlgn)。堆排序利用“堆”這種資料結構管理演算法執行中的資訊。堆這種資料結構不只是在堆排序中有用,還可以構成一個有效的優先佇列。

堆:

(二叉)堆資料結構是一種陣列物件,它可以是一個完全二叉樹。樹中的每個結點與陣列中存放的那個元素對應。樹的每一層都是填滿的。最後一層(葉子結點)除外。(最後一層從一個結點的左子樹開始填)。如圖:
表示堆的陣列A是一個具有兩個屬性的物件: length(A)是陣列中的元素個數, heap-size[A]是存放在A中的堆的元素個數。就是說,雖然A[1 .. length[A]]中都可以包含有效值,但A[heap-size[A]]之後的元素都不屬於相應的堆,此處heap-size[A]<=length[A]。樹的根為A[1]。 給定了某個元素的下標i, 其父節點PARENT(i), 左兒子LEFT(i)和右兒子RIGHT(i)的下標可以簡單的計算出來:

在大多數計算機上,LEFT過程計算2i,可將i的二進位制表示左移一位。RIGHT過程則將i左移一位並在地位中加1.PARENT過程可以把i右移一位。  二叉堆有兩種: 最大堆和最小堆(有的書上叫大頂堆和小頂堆),在最大堆中,父結點大於子結點 A[PARENT(i)] >=A[i], 反之,最小堆中A[PARENT(i)] <=A[i]. 在堆排序中,我們用的是最大堆,最小堆通常在構造優先佇列時使用。 堆可以被看做一棵樹,結點在堆中的高度定義為本結點到葉子的最長簡單下降路徑上邊的數目。堆的高度為樹根的高度。因為有n個元素的堆是基於一棵完全二叉樹的,其高度為O(lgn)。堆結構上一些基本操作的時間最多與堆的高度成正比。

保持堆的性質

MAX-HEAPIFY的輸入為陣列A和下標i, 當A[i]不符合最大堆的序列是,調整A[i]中元素的順序,直至符合要求。 如圖所示:
可見,當構造的二叉樹不是大頂堆時,則違反規則的結點和其左子樹和右子數比較找出最大的元素交換位置,直到構建的數符合標準。 演算法過程如圖:

建堆

我們可以自底向上的用MAX-HEAPIFY來將一個數組A[1 .. n] 變成一個最大堆。子陣列A[(n/2)+1 .. n]中的元素都是樹中的葉子,因此每個都可以看做是隻含有一個元素的堆。建堆的過程中對樹中的每個結點呼叫一次MAX-HEAPIFY 建堆的過程:
下圖為一個完整的建堆過程:

初始化: 在第一輪迭代之前 i = 【n/2】. 結點 n=【n/2】+1 ... n都是葉節點也是平凡大頂堆的根 保持:  要證明每次迭代都保持了迴圈不變式,注意到結點 i 的子結點的編號均比 i 大。於是,根據迴圈不變式, 這些子結點都是最大堆的根。這也是呼叫函式MAX-HEAPIFY(A,i), 以使結點 i 成為最大堆的根的前提條件。 此外, MAX-HEAPIFY(A,i)的呼叫保持了結點i+1, i+2,...,n為最大根的性質。在for迴圈中遞減i, 即為下一次迭代重新建立了迴圈不變式。 終止: 過程終止時,i=0. 根據迴圈不變式,我們知道結點1,2, ... ,n中,每個都是最大的根。特別的,結點1就是最大的根。

堆排序演算法

開始時,堆排序演算法先用BUILD-MAP-HEAP將輸入資料A[1 .. n]構造成一個大頂堆。因為陣列中最大的元素在根A[1], 則可以通過把它與A[n]呼喚來達到最終正確的位置。 現在,如果從堆中“去掉”結點n, 可以很容易的將A[1 .. n-1]建成大頂堆。原來的子女仍是大頂堆。而新元素違背了大頂堆的性質。這是呼叫MAX-HEAPIFY(A,1),就可以保持這一性質。在A{1 .. n-1}中構造出大頂堆。堆排序演算法不斷重複這個過程。堆的大小由n-1 一直降到2. 其過程如下:
下圖為一個完整的排序過程:
完整的實現程式碼:
/*
堆排序演算法實現
*/
#include<iostream>

using namespace std;


int HeapSize = 10;  // 暫定為這個數,假設需要排序的數有10個

/*
這三個過程在一個好的堆排序中通過內聯和巨集實現
*/
//父結點
int Parent(int i)
{
	return i/2;
}

//左子結點
int Left(int i)
{
	return 2*i;
}

//右子節點
int Right(int i)
{
	return 2*i+1;
}

// 維持大頂堆

void MaxHeapify(int A[], int i)
{
	int largest;    //臨時變數存放A[i]及左右子樹的最大值的下標
	int l = Left(i); //用l存放A[i]的左子樹
	int r = Right(i); //用r存放A[i]的右子樹
	
	// 判斷A[i]與其左子樹的大小
	// 將其中的最大值賦給largest
	if(A[l] > A[i] && l < HeapSize)
	{
		largest = l; 
	}
	else
	{
		largest = i;
	}
	// 判斷A[i]與其右子樹的大小
	// 將其中的最大值賦給largest
	if(A[r] > A[largest] && r <= HeapSize)
	{
		largest = r;
	}
	
	// 如果largest不是父結點A[i],則交換A[largest]與A[i]的位置
	int temp;
	if(largest != i)
	{
		temp = A[i];
		A[i] = A[largest];
		A[largest] = temp;
		// 繼續向下一層的子樹尋找
		MaxHeapify(A, largest);
	}
}

// 建堆
void BuildMaxHeap(int A[], int i) // n為陣列的長度,省去了演算法描述的第一步
{
	//每一次迭代開始時,結點i+1,i+2,...,n都是大頂堆的根
	for(i = i/2; i > 0; i--)
	{
		MaxHeapify(A, i);
	}
}

// 實現堆排序
/*
這裡A陣列從1開始計數,也就是A【0】元素不要。
取A[1,..,n]
呼叫函式HeapSort(A, HeapSize);
*/ 
void HeapSort(int A[], int n)
{
	int temp;
	BuildMaxHeap(A, n); // 構造大頂堆
	for(int i = n; i > 1; i--)
	{
		//將最後一個元素與堆的第一個元素交換位置
		temp = A[1];
		A[1] = A[i];
		A[i] = temp;
		// 堆的=規模隨之縮小1個
		HeapSize = HeapSize-1;
		// 重新維持堆
		MaxHeapify(A,1);
	}

}

注: 這裡只是簡單實現了《演算法導論》中的演算法。有些地方就偷工減料了。 結果:A【0】不參加排序。