1. 程式人生 > >排序演算法簡解

排序演算法簡解

目錄

演算法穩定性

堆排序

快速排序

歸併排序

演算法穩定性

  排序演算法穩定性的簡單形式化定義為:如果Ai = Aj,排序前Ai在Aj之前,排序後Ai還在Aj之前,則稱這種排序演算法是穩定的。通俗地講就是保證排序前後兩個相等的數的相對順序不變。

  對於不穩定的排序演算法,只要舉出一個例項,即可說明它的不穩定性;而對於穩定的排序演算法,必須對演算法進行分析從而得到穩定的特性。需要注意的是,排序演算法是否為穩定的是由具體演算法決定的,不穩定的演算法在某種條件下可以變為穩定的演算法,而穩定的演算法在某種條件下也可以變為不穩定的演算法。

  例如,對於氣泡排序,原本是穩定的排序演算法,如果將記錄交換的條件改成A[i] >= A[i + 1],則兩個相等的記錄就會交換位置,從而變成不穩定的排序演算法。

  其次,說一下排序演算法穩定性的好處。排序演算法如果是穩定的,那麼從一個鍵上排序,然後再從另一個鍵上排序,前一個鍵排序的結果可以為後一個鍵排序所用。基數排序就是這樣,先按低位排序,逐次按高位排序,低位排序後元素的順序在高位也相同時是不會改變的。

堆排序

堆排序利用了大根堆(或小根堆)堆頂記錄的關鍵字最大(或最小)這一特徵,使得在當前無序區中選取最大(或最小)關鍵字的記錄變得簡單。堆排序是利用這種資料結構而設計的一種排序演算法,堆排序是一種選擇排序,它的最壞,最好,平均時間複雜度均為O(nlogn),它是不穩定排序(發生在堆頂元素和最後無序元素髮生置換時),不適合記錄較少的排序。

呼叫堆的複雜度為O(log(n)),建立堆的複雜度為O(n),推導公式隨後給出。

堆實際上是一棵完全二叉樹。 
堆滿足兩個性質: 
(1) 堆的每一個父節點都大於(或小於)其子節點; 
(2) 堆的每個左子樹和右子樹也是一個堆。 

堆是具有以下性質:每個結點的值都大於或等於其左右孩子結點的值,稱為大頂堆;或者每個結點的值都小於或等於其左右孩子結點的值,稱為小頂堆

堆排序的基本思想是:將待排序序列構造成一個大頂堆,此時,整個序列的最大值就是堆頂的根節點。將其與末尾元素進行交換,此時末尾就為最大值。然後將剩餘n-1個元素重新構造成一個堆,這樣會得到n個元素的次小值。如此反覆執行,便能得到一個有序序列了。步驟:1.建立堆;2.調整堆;3.堆排序。

程式碼如下:

#include<iostream>
#include<algorithm>
using namespace std;
/*調整堆*/
void heapAdjust(int a[], int i, int size)
{
	int leftChild = 2 * i + 1;			//根節點序號為0,size只做判斷用.
	int rightChild = 2 * i + 2;
	int max = i;
	if (i <= size / 2 - 1)				//非葉節點判斷,可以防止對葉節點遞迴
	{
		if (leftChild < size && a[leftChild] > a[max])//只記錄位置,並未進行元素交換
			max = leftChild;
		if (rightChild < size && a[rightChild] > a[max])
			max = rightChild;
		if (max != i)				//如果max==i無需調整,即是區域性有序,調整是從指定或者變動元素開始依次遞迴為子節點。
		{
			swap(a[max], a[i]);
			heapAdjust(a, max, size);//調整交換後的以較大值位置為父節點的子堆的有序性。
		}
			
	}
	
}
/*建立堆*/
void creatHeap(int a[], int size)
{
	int i;
	for (i = size / 2 - 1; i >= 0; i--)
	{
		heapAdjust(a, i, size);//此過程從最後非葉節點開始調整。

	}
}
/*堆排序*/
void heapSort(int a[], int size)
{
	int iLoop;							//實時遞減的無序區堆大小,也即迴圈次數n
	creatHeap(a, size);				//建立大頂堆也是不斷調整區域性值得過程,之後進行最大元素(根節點元素)歸位。
	for (iLoop = size; iLoop >= 1; iLoop--)
	{
		swap(a[0], a[iLoop -1]);			//交換大頂堆根節點和無序區最後葉節點。
		heapAdjust(a, 0, iLoop -1);		//將根節點歸位後的堆重新調序為大頂堆,無序堆元素減一,此過程從根節點開始調整。(這裡iLoop從size開始遞減更容易理解,iLoop -1為容積減小1的堆大小)
	}
	
	
}
int main()
{
	int size;
	while (cin >> size)
	{
		int A[100], i;
		for (i = 0; i < size; i++)
			cin >> A[i];
		heapSort(A, size);
		for (i = 0; i < size; i++)
			cout << A[i] << ' ';
		cout << endl;
	}
	system("pause");
}

快速排序

歸併排序和快速排序都是一種分治思想的遞迴排序。

快速排序是由東尼·霍爾所發展的一種排序演算法。在平均狀況下,排序n個元素要O(nlogn)次比較。在最壞狀況下則需要O(n^2)次比較,但這種狀況並不常見。事實上,快速排序通常明顯比其他O(nlogn)演算法更快,因為它的內部迴圈可以在大部分的架構上很有效率地被實現出來。

快速排序的精髓在於參考值的選取,可以採用三數中值分割法來選取合適的參考值。具體步驟為,選取陣列元素頭尾和中間數[left+right]/2三個資料的中位數作為參考值。注意此時三個元素已經有序,可以節約時間。

程式碼如下(暫未進行參考元素優化):

#include<iostream>
#include<algorithm>
using namespace std;
void quickSort(int *a, int left, int right)
{
	/*基準值選擇為最左邊元素a[left]*/
	int i, j;
	if (left>right)
		return;							//不滿足條件退出
	i = left;
	j = right;
	while (i != j)
	{
		while (i < j&&a[j] >= a[left])
			j--;						//尋找每次迴圈第一個小於基準值的元素位置j
		while (i < j&&a[i] <= a[left])
			i++;						//尋找每次迴圈第一個大於基準值的元素位置j
		if (i < j)						//滿足條件,交換兩個元素的值
			swap(a[i], a[j]);			//交換每次迴圈找到的第一個大於和小於基準元素的兩個元素的值
	}
	swap(a[i], a[left]);				//交換基準元素和每一輪迭代滿足條件的最後一輪迴圈的小於基準元素的值,即將基準元素歸位。(注意若基準元素為最右側值,則改為最後一輪迴圈大於基準元素的值)
	quickSort(a, left, i - 1);			//分治思想的體現
	quickSort(a,  i + 1, right);
}
int main()
{
	int size;
	while (cin >> size)
	{
		int A[100], i;
		for (i = 0; i < size; i++)
			cin >> A[i];
		quickSort(A, 0, size - 1);
		for (i = 0; i < size; i++)
			cout << A[i] << ' ';
		cout << endl;
	}
	system("pause");
}

C++ STL庫中sort實現機制 

歸併排序

歸併排序是建立在歸併操作上的一種有效的排序演算法,效率為O(nlogn),1945年由馮·諾伊曼首次提出。

歸併排序演算法主要依賴歸併(Merge)操作。歸併操作是指合併兩個已經排序好的表,因為兩個表示排序好的,所以若將輸出放在第三個表中時,該演算法可以通過對輸入資料一趟排序完成。

步驟:

1.建立動態陣列,用於存放所有歸併操作後的合併陣列。

2.分治遞迴,將陣列遞迴分解為單一元素。

3.遞迴返回,進行歸併操作。注意所有歸併後元素都存放在唯一動態分配的記憶體中。

程式碼如下(記憶體優化):


/*此歷程進行記憶體優化,參考資料結構和演算法分析P175,常規Merge操作會每一次內部聲名一個臨時陣列,
這樣任一時刻最多可存在LOG(N)個臨時陣列。(Merge發生在所有遞迴條件不滿足即只剩下一個元素,則開始返回並進行歸併。)
佔用記憶體過多。 若進行動態記憶體獲取和釋放則很浪費時間,丟失了時間效率。
嚴密測試支出,由於Merge操作位於遞迴函式的最後一步,因此任一時刻只需要一個臨時陣列活動,而且可以使用該臨時陣列的任一部分,則最為簡便。
*/
#include<iostream>
using namespace std;
void mergeOperation(int a[],int pTemp[],int firstPartBegain,int secondPartBegain,int secondPartEnd)
{
	int i, firstPartEnd, size, tempPos;
	firstPartEnd = secondPartBegain - 1;
	tempPos = firstPartBegain;
	size = secondPartEnd - firstPartBegain+1; //該次遞迴返回的陣列大下。
	while (firstPartBegain <= firstPartEnd && secondPartBegain <= secondPartEnd)
		if (a[firstPartBegain] <= a[secondPartBegain])
			pTemp[tempPos++] = a[firstPartBegain++];
		else
			pTemp[tempPos++] = a[secondPartBegain++];
	while(firstPartBegain <= firstPartEnd)
		pTemp[tempPos++] = a[firstPartBegain++];//若第一部分剩餘,將第一部分剩下的補到動態臨時陣列中。
	while (secondPartBegain <= secondPartEnd)
		pTemp[tempPos++] = a[secondPartBegain++];//同理
	for (i = 0; i < size; i++,secondPartEnd--)
		a[secondPartEnd] = pTemp[secondPartEnd];//注意只有secondPartEnd是未改變的定值,firstPartBegain已經自增到firstPartEnd。

}
void mergeSortRecursion(int a[],int pTemp[],int left,int right)
{
	int middle;
	if (left < right)
	{
		middle = (left + right) / 2;				//第一部分尾部位置
		mergeSortRecursion(a, pTemp ,left, middle );//第一部分分治
		mergeSortRecursion(a, pTemp ,middle+1, right);//第二部分分治
		mergeOperation(a, pTemp, left, middle+1, right);
	}
}
void mergeSort(int a[], int size)
{

	int *pTemp = new int[size * sizeof(int)];
	if (pTemp != NULL)
	{
		mergeSortRecursion(a, pTemp, 0, size - 1);
		delete [] pTemp;
	}
	else
		cout <<"no space for users to achieve,fatal error appeared"<<endl;

}

int main()
{
	int size;
	while (cin >> size)
	{
		int A[100];
		for (int i = 0; i < size; i++)
			cin >> A[i];
		mergeSort(A, size);
		for (int i = 0; i < size; i++)
			cout << A[i]<<' ';
		cout << endl;
	}
	system("pause");
	return 0;
}