快速排序相關——基本快排實現,優化,第K大數
快速排序
//部分參考維基百科 https://zh.wikipedia.org/wiki/
目錄
基本介紹
在平均狀況下,排序個專案要次比較。在最壞狀況下則需要次比較,但這種狀況並不常見。事實上,快速排序通常明顯比其他演算法更快,因為它的內部迴圈(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) | 看實現 | 不穩定 |