1. 程式人生 > >各種排序演算法時間複雜度、穩定性、初始序列是否對元素比較次數有關

各種排序演算法時間複雜度、穩定性、初始序列是否對元素比較次數有關

怎麼記憶穩定性

總過四大類排序:插入、選擇、交換、歸併(基數排序暫且不算)

比較高階一點的(時間複雜度低一點得)shell排序,堆排序,快速排序(除了歸併排序)都是不穩定的,在加上低一級的選擇排序是不穩定的。

比較低階一點的(時間複雜度高一點的)插入排序,               氣泡排序,歸併排序,基數排序都是穩定的。

(4種不穩定,4種穩定)。

怎麼記憶初始序列是否對元素的比較次數有關:

/**
  * @brief 嚴版資料結構書程式碼
  *        最好的情況,陣列本身有序,就只需執行n-1次比較,此時時間複雜度為O(n);
  *        最壞的情況,陣列本身逆序,要執行n(n-1)/2次,此時時間複雜度為O(n^2);
  */
void _insertSort(int R[], int n)
{
	int i, j, temp;
	for ( i = 1; i < n; ++i ) {
		if ( R[i] < R[i - 1] ) {//將R[i]插入有序字表
			temp = R[i];		//設定哨兵
			for ( j = i - 1; R[j] > temp; --j ) {
				R[j+1] = R[j];
			}
			R[j+1] = temp;
		}
	}
}

對於直接插入排序:

當最好的情況,如果原來本身就是有序的,比較次數為n-1次(分析(while (j >= 0 && temp < R[j]))這條語句),時間複雜度為O(n)。

當最壞的情況,原來為逆序,比較次數為2+3+...+n=(n+2)(n-1)/2次,而記錄的移動次數為i+1(i=1,2...n)=(n+4)(n-1)/2次。

如果序列是隨機的,根據概率相同的原則,平均比較和移動的次數為n^2/4.

/**
  * @brief 嚴版資料結構 選擇排序
  *        採用"選擇排序"對長度為n的陣列進行排序,時間複雜度最好,最壞都是O(n^2)
  *        當最好的時候,交換次數為0次,比較次數為n(n-1)/2
  *        最差的時候,也就初始降序時,交換次數為n-1次,最終的排序時間是比較與交換的次數總和,
  *        總的時間複雜度依然為O(n^2)
  */
void _selectSort(int R[], int n)
{
	int i, j, temp, index;
	for ( i = 0; i < n; ++i ) {
		index = i;
		for ( j = i + 1; j < n; ++j ) {
			if ( R[index] > R[j] ) {
				index = j;//index中存放關鍵碼最小記錄的下標
			}
		}
		if (index != i) {
			temp = R[i];
			R[i] = R[index];
			R[index] = temp;
		}
	}
}
選擇排序不關心表的初始次序,它的最壞情況的排序時間與其最佳情況沒多少區別,其比較次數都為 n(n-1)/2,交換次數最好的時候為0,最差的時候為n-1,儘管和氣泡排序同為O(n),但簡單選擇排序效能上要優於氣泡排序。但選擇排序可以   非常有效的移動元素。因此對次序近乎正確的表,選擇排序可能比插入排序慢很多。
/**
  * @brief     改進的氣泡排序
  * @attention 時間複雜度,最好的情況,要排序的表本身有序,比較次數n-1,沒有資料交換,時間複雜度O(n)。
  *            最壞的情況,要排序的表本身逆序,需要比較n(n-1)/2次,並做等數量級的記錄移動,總時間複雜度為O(n^2).
  */
void bubbleSort2(int R[], int n)
{
	int i, j, temp;
	bool flag = TRUE;	//flag用來作為標記

	for ( i = 0; i < n && flag; ++i ) {
		flag = FALSE;
		for ( j = n - 1; j > i; --j ) {
			if (R[j] < R[j - 1]) {
				temp = R[j];
				R[j] = R[j - 1];
				R[j - 1] = temp;
				flag = TRUE;//如果有資料交換,則flag為true
			}
		}
	}
}
氣泡排序:

最好的情況,n-1次比較,移動次數為0,時間複雜度為O(n)。

最壞的情況,n(n-1)/2次比較,等數量級的移動,時間複雜度為O(O^2)。

/**
  * @brief 希爾排序, 對於長度為n的陣列,經過 "希爾排序" 輸出
  */
void shellSort(int R[], int n)
{
	int i, j, temp;
	int k = n / 2;
	while (k >= 1) {
		for (i = k; i < n; ++i) {
			temp = R[i];
			j = i - k;
			while (R[j] < temp && j >= 0) {
				R[j+k] = R[j];
				j = j - k;
			}
			R[j+k] = temp;
		}
		k = k / 2;
	}

希爾排序初始序列對元素的比較次數有關。

/**
  * @brief     構建 大頂堆
  * @attention 個人版本,堆排序
  */
void heapAdjust(int R[], int start, int end)
{
	int j, temp;
	temp = R[start];
	for ( j = 2 * start + 1; j <= end; j = j * 2 + 1 ) {
		
		if ( j < end && R[j] < R[j + 1] ) {
			++j;
		}
		if ( temp >  R[j] ) {
			break;
		}
		R[start] = R[j];
		start = j;
	}
	R[start] = temp;
}

/**
  * @brief 堆排序
  * @param R為待排序的陣列,size為陣列的長度
  *  時間複雜度:構建大(小)頂堆,完全二叉樹的高度為log(n+1),因此對每個結點調整的時間複雜度為O(logn)
  *           兩個迴圈,第一個迴圈做的操作次數為n/2,第二個操作次數為(n-1),因此時間複雜度為O(nlogn)
  */
void heapSort(int R[], int size)
{
	int i, temp;
	for ( i = size / 2 - 1; i >= 0; --i ) {
		heapAdjust(R, i, size);
	}
	for ( i = size - 1; i >= 0; --i ) {
		temp = R[i];
		R[i] = R[0];
		R[0] = temp;//表尾和表首的元素交換
		heapAdjust(R, 0, i - 1);//把表首的元素換成表尾的元素後,重新構成大頂堆,因為除表首的元素外,
								//後面的結點都滿足大頂堆的條件,故heapAdjust()的第二個引數只需為0
	}
}
/**
  * @brief 將有序的長度為n的陣列a[]和長度為m的b[]歸併為有序的陣列c[]
  *        只要從比較二個數列的第一個數,誰小就先取誰,取了之後在對應的數列中刪除這個數。
  *        然後再進行比較,如果有數列為空,那直接將另一個數列的資料依次取出即可。
  *        將兩個有序序列a[first...mid]和a[mid...last]合併
  */
void mergeArray(int a[], int first, int mid, int last, int tmp[])
{
	int i = first, j = mid + 1;
	int k = 0;
	while ( i <= mid && j <= last ) {
		if ( a[i] <= a[j] )
			tmp[k++] = a[i++];
		else
			tmp[k++] = a[j++];
	}
	while ( i <= mid ) {
		tmp[k++] = a[i++];
	}
	while ( j <= last ) {
		tmp[k++] = a[j++];
	}
	for (i = 0; i < k; i++) {//這裡千萬不能丟了這個
        a[first + i] = tmp[i];
	}
}
/**
  * @brief 歸併排序,其的基本思路就是將陣列分成二組A,B,如果這二組組內的資料都是有序的,
  *        那麼就可以很方便的將這二組資料進行排序。如何讓這二組組內資料有序了?

  *        可以將A,B組各自再分成二組。依次類推,當分出來的小組只有一個數據時,
  *        可以認為這個小組組內已經達到了有序,然後再合併相鄰的二個小組就可以了。這樣通過先 (遞迴) 的分解數列,
  *        再 (合併) 數列就完成了歸併排序。
  */
void mergeSort(int a[], int first, int last, int tmp[])
{
	int mid;
	if ( first < last ) {
		mid = ( first + last ) / 2;
		mergeSort(a, first, mid, tmp);	//左邊有序
		mergeSort(a, mid + 1, last, tmp);	//右邊有序
		mergeArray(a, first, mid, last, tmp);
	}
}

/** 
  * @brief 雖然快速排序稱為分治法,但分治法這三個字顯然無法很好的概括快速排序的全部步驟。
  *        因此我的對快速排序作了進一步的說明:挖坑填數+分治法:
  * @param R為待排陣列,low和high為無序區
  *        時間複雜度:最好O(nlogn),最壞O(n^2),平均O(nlogn),空間複雜度O(logn);
  */
void quickSort(int R[], int low, int high)
{
	if ( low < high ) {
		int i = low, j = high, temp = R[low];

		while ( i < j ) {
			//從右往左掃描,如果陣列元素大於temp,則繼續,直至找到第一個小於temp的元素
			while ( i < j && R[j] >= temp ) {
				--j;
			}
			if ( i < j ) {
				R[i++] = R[j];
			}
			while ( i < j && R[i] <= temp ) {
				++i;
			}
			if ( i < j ) {
				R[j--] = R[i];
			}
		}
		R[i] = temp;
		quickSort(R, low, i - 1);
		quickSort(R, i + 1, high);
	}
}

各排序演算法整體分析

氣泡排序、插入排序、希爾排序以及快速排序對資料的有序性比較敏感,尤其是氣泡排序和插入排序;

 選擇排序不關心表的初始次序,它的最壞情況的排序時間與其最佳情況沒多少區別,其比較次數為 n(n-1)/2,但選擇排序可以   非常有效的移動元素。因此對次序近乎正確的表,選擇排序可能比插入排序慢很多。

氣泡排序在最優情況下只需要經過n-1次比較即可得出結果(即對於完全正序的表),最壞情況下也要進行n(n-1)/2 次比較,與選擇排序的比較次數相同,但資料交換的次數要多餘選擇排序,因為選擇排序的資料交換次數頂多為 n-1,而氣泡排序最壞情況下的資料交換n(n-1)/2 。氣泡排序不一定要進行 趟,但由於它的記錄移動次數較多,所以它的平均時間效能比插入排序要差一些。

插入排序在最好的情況下有最少的比較次數 ,但是它在元素移動方面效率非常低下,因為它只與毗鄰的元素進行比較,效率比較低。

希爾排序實際上是預處理階段優化後的插入排序,一般而言,在 比較大時,希爾排序要明顯優於插入排序

快速排序採用的“大事化小,小事化了”的思想,用遞迴的方法,將原問題分解成若干規模較小但與原問題相似的子問題進行求解。快速演算法的平均時間複雜度為O(nlogn) ,平均而言,快速排序是基於關鍵字比較的內部排序演算法中速度最快者;但是由於快速排序採用的是遞迴的方法,因此當序列的長度比較大時,對系統棧佔用會比較多。快速演算法尤其適用於隨機序列的排序。

因此,平均而言,對於一般的隨機序列順序表而言,上述幾種排序演算法效能從低到高的順序大致為:氣泡排序、插入排序、選擇排序、希爾排序、快速排序但這個優劣順序不是絕對的,在不同的情況下,甚至可能出現完全的效能逆轉。

對於序列初始狀態基本有正序,可選擇對有序性較敏感的如插入排序、氣泡排序、選擇排序等方法

對於序列長度 比較大的隨機序列,應選擇平均時間複雜度較小的快速排序方法

各種排序演算法都有各自的優缺點,適應於不同的應用環境,因此在選擇一種排序演算法解決實際問題之前,應當先分析實際問題的型別,再結合各演算法的特點,選擇一種合適的演算法

       這裡特別介紹下快速排序:

快速排序的時間主要耗費在劃分操作上,對長度為k的區間進行劃分,需要k-1次關鍵字比較。

1)最壞的時間複雜度

最壞情況是每次劃分選取的基準都是當前無序區中關鍵字最小(或最大)的記錄,劃分的結果是基準左邊的子區間為空(或右邊的子區間為空),而劃分所得的另一個非空的子區間中記錄數目,僅僅比劃分前的的無序區中記錄個數減少一個。

    因此,快速排序必須做n-1次劃分,第i次劃分開始區間長度為n-i+1,所需的比較次數為n-i(1<=i<=n-1),故總的比較次數達到最大值:n(n-1)/2;

    如果按上面給出的劃分演算法,每次取當前無序區的第1個記錄為基準,那麼當檔案的記錄已按遞增序(或遞減序)排列時,每次劃分所取的基準就是當前無序區中關鍵字最小(或最大)的記錄,則快速排序所需的比較次數反而最多。

(2)最壞的時間複雜度

在最好情況下,每次劃分所取的基準都是當前無序區的"中值"記錄,劃分的結果是基準的左、右兩個無序子區間的長度大致相等。總的關鍵字比較次數:

        0(nlgn)

(3)平均時間複雜度

    儘管快速排序的最壞時間為O(n2),但就平均效能而言,它是基於關鍵字比較的內部排序演算法中速度最快者,快速排序亦因此而得名。它的平均時間複雜度為O(nlgn)。

 (4)空間複雜度

    快速排序在系統內部需要一個棧來實現遞迴。若每次劃分較為均勻,則其遞迴樹的高度為O(lgn),故遞迴後需棧空間為O(lgn)。最壞情況下,遞迴樹的高度為O(n),所需的棧空間為O(n)。