1. 程式人生 > >內部排序(三)堆排序的兩種實現

內部排序(三)堆排序的兩種實現

堆排序是一種選擇排序演算法,堆排序顧名思義要用到堆,首先來回顧下有關資料結構“堆”有哪些特點。

  1. 堆常用二叉樹來表示,而且如果不是特殊情況的話,通常用一棵完全二叉樹來表示堆。因為完全二叉樹的結點分佈均勻,所以通常可以用陣列來實現堆的儲存。
  2. 根據堆中任一結點和其他結點的值的關係,堆分成兩種,最大堆和最小堆。最大堆指堆中任一結點的值都大於其子結點的值;最小堆則相反,堆中任一結點的值都小於其子結點的值。

看到堆的特點之後,是不是冥冥之中覺得這種任一結點比其子結點大/小的特性或許可以用來做排序?對的,堆排序是選擇排序的一種,選擇排序大家很容易理解,一種“暴力”的選擇排序方法就是每次掃描一遍整個待排序列,從中找出最大/最小值,然後儲存到一個額外空間中,接著再把待排序列剩下的元素每個掃描一遍,再從中找出最大/最小的元素,這樣一直下去直到把待排序列所有元素“清空”。

      但是我們知道每一都掃描一遍整個待排序列來找出最大/最小元素,肯定是不合適的。那麼我們想如何可以加快尋找最大值/最小值這一步驟?

      那就是用最大堆或最小堆了!因為最大堆的根結點一定就是存放的最大元素,最小堆的根結點一定就是存放的最小元素。所以就可以利用最大堆/最小堆來快速獲取待排序列中的最值,從而加快排序的速度!這就是堆排序。

      那麼如何做堆排序呢?假設我們要對序列做升序排序(降序同理),第一種方法,我們可以把待排序列中元素一個一個拿出來建成一個最大堆,這樣每次從最大堆從彈出一個最大元素,然後按升序方式放回我們的待排序列中,就可以了。這樣真的是可以,實現程式碼如下:

/*主函式*/
int main(int agrc, char const* argv[]) 
{
	bool IsFull, IsEmpty;
	int i, j;
	ElemType InsertData, DeleteItem, data=0;
	
	/*建立一個堆序列,把待排序列一個一個插入到堆中並調整成最大堆*/ 
	PtrlSqList H=CreatList(); 
	/*建立一個待排序列*/
	PtrlSqList P=CreatList();
	printf("請建立待排序列:");
	while (data!=-1) {
		scanf("%d", &data);
		if (data!=-1) {
			Insert_Array(P, data);
		}
	} 
	/*獲得待排序列長度*/
	P->length=GetListLength(P);
	/*輸出待排序列*/
	printf("\n待排序列為:");
	PrintList(P, 0); 
	printf("\n待排序列長度=%d\n", P->length); 
	
	/*把待排序列元素一個個插入到堆中同時做調整*/
	/*for迴圈中i<=P->length+1是為了把-1結束標誌也存進去*/
	for (i=1; i<=P->length+1; i++) {
		Insert(H, P->arr[i]);
		P->arr[i]=0;
		j=i;
	} 
	P->arr[j]=-1; /*結束符*/
	
	printf("\n最大堆H:");
	PrintList(H, 0);
	/*獲取最大堆的長度*/
	H->length=GetListLength(H);
	printf("\n最大堆H的長度=%d\n", H->length); 

	printf("\n迴歸過程:\n");
	j=P->length;
	for (i=1; i<=P->length; i++) {
		P->arr[j]=Delete(H);
		PrintList(P, 0);
		printf("\n");
		j--;
	}
	
	printf("\n堆排序後序列:");
	PrintList(P, 0);
	return 0;
} 

建立一個空堆H和待排序列P,第55行開始for迴圈把待排序列P中的元素一個一個拿出來插入到堆H中同時每次插入後都調整堆H為最大堆。把待排序列全部插入調整成一個最大堆後,就可以開始從最大堆不斷彈出堆頂元素,按需要的排序方式存放回序列P中:

如何在插入一個元素到堆中後同時把堆調整成最大堆?我們來看程式碼:

/*判斷最大堆是否已滿*/
bool IsFull(PtrlSqList P) 
{
	return (P->MaxContent==P->NowSize);
} 

/*判斷最大堆是否為空*/
bool IsEmpty(PtrlSqList P)
{
	return (P->NowSize==0);
}

/*最大堆的插入*/
void Insert(PtrlSqList H, ElemType X)
{
	int i;
	/*插入前先判斷堆滿不滿*/
	if (IsFull(H)) {
		printf("最大堆已滿,無法插入.");
		return;
	}
	/*沒有滿就插入*/
	i=++H->NowSize; /*因為NowSize記錄的是當前堆中最後一個元素的下標,所以在它後面做插入*/
	 
	/*調整插入的位置,如果i位置的父結點比插入的值小,就把父結點移動下來*/
	/*i/2是i的父結點,2i是左子樹,2i+1是右子樹*/
	for (; H->arr[i/2]<X; i/=2) {
		H->arr[i]=H->arr[i/2]; /*把父結點往下移*/
	} 
	/*i的父結點不比要插入的X小了,那麼就插入X到i*/
	H->arr[i]=X;
}

第162行插入元素前首先判斷堆滿不滿,沒滿就讓i=++H->NowSize。然後開始找插入的位置,第170行從當前堆中最後一個位置的父結點開始,如果父結點的值比要插入的X小,那麼就把父結點下移,讓X到父結點的位置,父結點下移即把堆中父結點H->arr[ i/2 ]的值賦給H->arr[ i]位置的值。然後i\=2,就是從i的父結點繼續往上找,直到找到父結點i比要插入的X大了,X就作為i的子結點插入。

至於在做完插入操作把待排序列建立成一個最大堆後,怎麼把最大堆堆頂元素一個一個彈出來?這樣實現:

/*最大堆的刪除(即彈出一個元素)*/ 
ElemType Delete(PtrlSqList H) 
{
	int Head, Max; /*Head是要插入的位置, Max是Head的子結點的下標位置*/
	ElemType DeleteItem, Tag;
	
	/*彈出一個元素前,先判斷堆為不為空*/
	if (IsEmpty(H)) {
		printf("最大堆為空.");
		return;
	}
	/*不為空就彈出元素*/
	DeleteItem=H->arr[1]; /*先把要彈出的結點儲存起來,最後返回出去*/
	Tag=H->arr[H->NowSize--]; /*儲存彈出一個元素後,堆中最後一個元素(最小元素)的下標*/
	
	/*下面的操作是把彈出一個元素後的堆調整回最大堆*/
	/*Head*2是判斷該結點是否有左子樹,Head*2是左子樹,Head*2+1是右子樹*/
	/*Head即要插入的位置從1開始,即從第一個頂點(根節點)開始判斷*/
	for (Head=1; Head*2<=H->NowSize; Head=Max) {
		/*如果有左子樹,就判斷左子樹和右子樹誰更大*/
		/*Head*2<=NowSize判斷當前要插入的位置是否有左子樹,如果左子樹==NowSize,則沒右子樹*/
		Max=Head*2; /*一開始先讓Max的值是左子樹的位置下標,即指向左子樹*/
		/*如果左子樹結點小於右子樹結點*/
		if ((H->arr[Max]<H->arr[Max+1]) && (Max!=H->NowSize)) {
			/*Max!=H->NowSize,證明有右子樹*/
			/*就把Max指向更大的右子樹的位置*/ 
			Max++;
		}
		/*一輪迴圈找到當前要插入的位置的左右子樹的更大的那個位置Max後*/ 
		/*比較我當前要插入的元素Tag是否比左右子樹大,是就直接插入*/ 
		if (Tag>=H->arr[Max]) {
			break;
		} else {
			/*否則就把更大的那個值提上來,調成最大堆的規律*/
			/*而我Tag要插入的位置下移,下移操作是for迴圈的最後Head=Max*/
			/*Head=Max即把Head的子樹位置給了自己,讓自己下移了*/
			H->arr[Head]=H->arr[Max]; 
		} 
	}
	/*for迴圈做完後,即找到正確的插入位置,就插入Tag元素*/
	H->arr[Head]=Tag;
	
	return DeleteItem;
}

一樣彈出元素前先判斷堆空不空,不空就把堆頂元素H->arr[ 1 ]賦值給DeleteItem變數儲存,最後返回出去。關鍵是彈出一個元素後,怎麼把堆調整回最大堆。

第181行定義了兩個變數Head和Max,Head是用來儲存查詢插入位置的,因為堆中彈出一個元素後,堆得大小就要-1,而堆的資料結構中NowSize是用來儲存堆中當前的容量,值就是堆中最後一個元素的位置的下標,所以再彈出了一個元素後,第191行我們把堆中最後一個元素的值賦給Tag’變數暫時保持,NowSize--,然後為這個Tag元素找插入的位置,同時調整堆。

首先讓Head=1就是從堆頂位置開始,如果Head*2<=H->NowSize,意思是判斷該結點有沒有左結點且有沒有超出堆的大小,如果沒有,就證明有左結點且有右結點(Head*2<=H->NowSize,小於NowSize很重要,因為左結點的位置下標是2i,右結點的位置下標是2i+1,所以如果左結點的下標沒有超出NowSize,就證明有右結點)。

然後第199行先讓Max指向左結點的位置,判斷如果左結點小於右結點,就讓Max指向右結點。這一步的做法是找出當前插入位置的子結點更大的結點,如果要插入的元素Tag比當前要插入的位置的子結點都大,如果是,就證明插入位置找到了,break停止迴圈,第220把Tag賦值給H->arr[ Head ]。如果Tag比插入位置Head的子結點要小,那麼就把子結點Max的位置賦值給Head,即往下尋找插入位置。

上面這種堆排序的做法,其實也不適合,因為我們看排序部分的程式碼,我們需要一個額外的空間堆H來存待排序列的元素,把待排序列建立成一個最大堆。最後還有把排好序後的最大堆中元素一個一個複製回去原序列中,尤其是最後的複製迴歸過程,當資料量很大時,這個資料複製時間就可能要很久,而且需要的額外空間堆H也可能很大。

為了去掉額外空間和重新複製元素回原序列,可以用第二種更好的方法,就是直接對待排序列動手。方法是,首先我們直接把整個待排序列調整成最大堆形式,這時堆頂元素就是最大值,因為我們要把待排序列排成升序,升序序列中最大的元素是排在最後的(可按需要改動程式碼實現其他規律的排序),所以,我們把堆頂元素和堆中最後一個元素例如N做交換,接著把最後一個元素之前的堆調整回一個最大堆。然後對N-1的堆中元素繼續把最大堆中第一個元素和最後一個元素,此時是N-1交換,這樣序列中第二大的元素就去到了序列的倒數第二個位置了。

一直做下去,把堆調整回最大堆,然後把第一個元素和N-2的位置的元素交換,直到所有元素排完序,最後直接逐個彈出堆中所有元素回原序列P中即可,我們來看看程式碼,看看它是怎麼回事:

/*堆排序*/
void Heap_Sort(PtrlSqList P) 
{
	int i=0; 
	/*從最後一個結點的父結點開始往上調整*/
	for (i=P->length/2; i>0; i--) {
		/*把待排序列中的元素調整成最大堆*/
		AdjustMaxHeap(P, i, P->length);
	}
	/*調整成最大堆後的序列*/
	printf("調整成最大堆後的序列為:");
	PrintList(P, 0); 
	
	printf("\n\n"); /*為了美觀的換行*/
	/*開始堆排序*/
	for (i=P->length; i>0; i--) {
		//printf("此時i=%d\n", i);
		Swap(&P->arr[1], &P->arr[i]);
		/*一趟堆排序後,把待排序列繼續調整回最大堆,做下一趟堆排序*/
		AdjustMaxHeap(P, 1, i-1);
		PrintList(P, (P->length-i+1));
		printf("\n");
	}
}

第144行把待排序列調整成最大堆,然後第156行for迴圈,從堆中最後一個元素的位置開始,即P->length,每次Swap函式是把堆中第一個元素和“最後一個”元素交換,交換完後,即最大元素落到最後一位了,就把之前的元素調整回最大堆。

Swap函式是使用者自定函式,目的是實現兩位置元素交換,比較簡單且方法很多,這裡就不展示了。

至於調整堆為最大堆函式AdjustMaxHead,我們來看怎麼做:

/*調整堆的有序性,調整為最大堆*/
void AdjustMaxHeap(PtrlSqList P, ElemType i, int length) 
{
	int Head, Max; /*作用和最大堆刪除函式中一樣*/
	ElemType Tmp; /*臨時儲存那個要調整位置的元素*/
	
	Tmp=P->arr[i]; /*把要調整位置的元素賦給一個臨時變數*/
	for (Head=i; Head*2<=length; Head=Max) {
		Max=Head*2;
		if (Max!=length && P->arr[Max]<P->arr[Max+1]) {
			Max++;
		}
		if (Tmp>P->arr[Max]) {
			break;
		} else {
			P->arr[Head]=P->arr[Max];
		}
	} 
	P->arr[Head]=Tmp;
}

方法和上面最大堆彈出一個元素調整位置一樣,只是傳入引數不同,最大堆的調整,因為我們每次排序都把最大元素放到最後的位置,交換了位置之後,堆中1的位置元素是當前待排序列最後一個元素,所以引數i是1,length是每次要調整的元素的個數。方法一模一樣,從位置1開始,如果左右結點比Tmp大,就把要插入的位置Head下移(否則直接插入),直到找到比位置Head的子結點大後,在位置Head處插入。

方法2就不需要額外的空間來暫存資料,也就省掉了把排好序的資料複製回原序列中的這費時間一步。

完整實現程式碼在個人程式碼雲: