1. 程式人生 > >快速排序相關——基本快排實現,優化,第K大數

快速排序相關——基本快排實現,優化,第K大數

快速排序

//部分參考維基百科 https://zh.wikipedia.org/wiki/

目錄

快速排序

基本介紹

整體的思路

程式碼實現

第K大數字:

最壞情況優化:

與堆排序、歸併排序的比較 


基本介紹

在平均狀況下,排序n個專案要{\displaystyle \ O(n\log n)}次比較。在最壞狀況下則需要{\displaystyle O(n^{2})}次比較,但這種狀況並不常見。事實上,快速排序{\displaystyle \Theta (n\log n)}通常明顯比其他演算法更快,因為它的內部迴圈(inner loop)可以在大部分的架構上很有效率地達成;快速排序是不穩定的

使用的思想是分治法,分成兩組,遞迴的解決

最壞情況下

出現在輸入完全逆序的情況下

每次劃分產生的兩組分別包含n-1個和0個元素,整個的執行時間遞迴式可以表示為:

T(n) = T(n-1)+θ(n),也就是O(n^2)級別

最好情況劃分

最平衡的劃分中,Partition得到的兩個子問題的規模都不大於n/2,這種情況下快速排序的效能非常好

T(n)=2*T(n/2)+θ(n),也即是θ(nlogn)

快速排序的平均執行時間更接近於其最好情況,基本上只要劃分是常數比例的,演算法的執行時間總是O(nlogn)

整體的思路

1、從待排序數列中選擇一個基準值(key)

2、重新排列陣列,使得比key小的元素在key的左邊,比key大的元素在key右邊,這個操作稱為分割(partition)

3、遞迴的把小於key的部分數列和大於key的部分數列進行重排

遞迴到最底部時,陣列的長度為0或1,該演算法一定會結束,因為至少會把一個元素擺到後面的位置去

C++中的 sort函式就是實現的快排演算法

程式碼實現

下面給出一種原址排序的C++實現方法:

p是第一個下標位置,r是最後一個數字的下標位置

在Partition中,以最後一個元素為key值進行劃分,在剩下的元素中找出比k小的元素,交換到左邊 --- 以i記錄比key小的最後一個元素的下標,j去找等待被交換回來的小於k的元素的下標

關鍵是記住i的初始化,先更新i再交換,以及最後返回的key在第i+1位置上

int Partition(int p, int r)
{
	int k = a[r];
	int i = p - 1;
	for (int j = p; j < r; j++)
	{
		if (a[j] <= k)
		{
			i++;
			swap(a[i], a[j]);
		}
	}
	swap(a[i + 1], a[r]);
	return i + 1;
}
void quickSort(int p, int r)
{
	if (p < r)
	{
		int q = Partition(p, r);
		quickSort(p, q - 1);
		quickSort(q + 1, r);
	}
}

另一種方法的區別是Partition部分,採用“挖坑填坑”的思想,用i,j分別從兩邊找,以最後一個為k的值的話,i從前往後找比k大的,j從後往前找比k小的,直到i==j為止

int Partition2(int p, int r)
{
	int k = a[r];
	int i = p, j = r;
	while (i<j)
	{
		while (i<j && a[i]<k)
		{
			i++;
		}
		if (i < j) { a[j] = a[i]; j--; }
		while (i<j && a[j]>k)
		{
			j--;
		}
		if (i < j) { a[i] = a[j]; i++; }
	}
	a[i] = k;
	return i;
}

第K大數字:

直接複用上面的Partition程式碼就可以

有幾點要注意:返回的索引q在當前數組裡是第key大的,不是直接比較q與k; 另外在k>key時,不在求的是第k大,而是第k-key大;

還有遞迴出口是p==r的情況

int selectKBiggest(int p, int r, int k)
{
	if (p == r)return a[p];
	int q = Partition(p, r);
	int key = q - p+1 ;
	if (k == key)return a[q];
	else if (k < key)
		return selectKBiggest(p, q - 1, k);
	else return selectKBiggest(q + 1, r, k-key);
}

非遞迴版本:

int selectKUnRec(int p, int r, int k)
{
	while (p <= r)
	{
		int q = Partition(p, r);
		int key = q - p + 1;
		if (k == key)return a[q];
		else if (k < key)r = q-1;
		else { p = q + 1; k = k - key; }
	}
}

只需要一個while就可以了,該上下下標和k值就好了

 

最壞情況優化:

分治方法,分成的兩個陣列差不多大時,效率是最高的,所以基準的選擇很重要;前面都是選擇了固定的最後一個元素作為劃分的基準;

在此基礎上稍作改進可以是隨機的選擇基準值,能一定程度提高,但是最壞的情況下還是O(n^2);

進一步的改進是選用三數取中的方法:

最佳的取值應當是待排序陣列分成相等的兩組,選擇中間那個數;但是要精確的找到中間那個數是很費時的,所以可以從三個數中選擇中間那個值,實際上選擇首、尾和中間值,找到這三個數中中間位置的數作為key即可,這樣可以消除預輸入不好的情況;當然也可以由三數取中換成五數取中等

但是三數取中優化不了重複陣列的情況

在這上面的進一步優化的考慮可以是,在劃分到一定程度後,用插入排序來替代繼續劃分,因為當陣列部分有序時,快排沒有插入排序效率高;

if (high - low + 1 < 10)
{
	InsertSort(arr,low,high);
	return;
}//else時,正常執行快排

或者在一次劃分結束後,將與key相等的元素聚集到一起,劃分時不對其進行劃分:  可以在劃分時,將與key相等的數移動到兩端,然後劃分結束後再移回中軸附近

還有一種優化思路是,在QuickSort中有兩次遞迴,可以改成一次尾遞迴:

void QSort(int arr[],int low,int high)
{ 
	int pivotPos = -1;
	if (high - low + 1 < 10)
	{
		InsertSort(arr,low,high);
		return;
	}
	while(low < high)
	{
		pivotPos = Partition(arr,low,high);
		QSort(arr,low,pivot-1);
		low = pivot + 1;
	}
}
--------------------- 
作者:insistgogo 
來源:CSDN 
原文:https://blog.csdn.net/insistGoGo/article/details/7785038 
版權宣告:本文為博主原創文章,轉載請附上博文連結!

上面的博主實驗結果:這裡效率最好的快排組合 是:三數取中+插排+聚集相等元素,它和STL中的Sort函式效率差不多

與堆排序、歸併排序的比較 

  平均時間複雜度 最好 最壞 輔助空間 穩定性
堆排序 O(nlogn) O(nlogn) O(nlogn) O(1) 不穩定
歸併排序 O(nlogn) O(nlogn) O(nlogn) O(1) 穩定
快速排序 O(nlogn) O(nlogn) O(n^2) 看實現 不穩定