關於快排的主體思想那自然不用說,但是具體代碼實現的細節確是很多。下面通過網上找的多個版本來找找其中的細節與優劣。相信只要你對這塊不是十分了解或者自己仔細琢磨過細節,那么閱讀本文肯定有所收獲。
轉載請注明,原文來自https://www.cnblogs.com/willhua/p/9426130.html
版本1
public static void quickSort(int[] a, int start, int end) {
if (start >= end) return;
int t = a[end];
int i = start - 1;
for (int j = start; j <= end - 1; j++) {
if (a[j] < t) {
i++;
swap(a, i, j);
}
}
swap(a, i + 1, end);
quickSort(a, start, i);
quickSort(a, i+2, end);
}
//來源:https://www.zhihu.com/question/24361443/answer/362777698
分析:這個的特點在於從一頭遍歷到尾來進行二分,而不像一般的從兩頭分別開始。對於核心的for循環,首先分析其if,它是當當前處理的元素如果小於t,那么就進行swap,如果大於或者等於t則不會進行交換,這樣的話,i就不會增長,也就意味着i表示的是第一個大於或等於t的元素所在的位置,即意味着(i, j)都是屬於大於或者等於t的元素。后續處理中,如果if條件滿足,那就相當於把第一個大於或者等於t的元素放到當前小於t的位置,同時把這個小於t的元素放到前面的小於t范圍的末位,形成了大於或者等於t的范圍的翻滾式的向后面移動或者前進的效果。
這種寫法從語言的簡潔上來說是比從兩頭分別處理的版本的好,但是這種寫法會比兩頭處理的版本更多次的swap,尤其考慮在數組的前面某個范圍,其數據本身都是小於t的,那么使用這種版本也會進行很多次swap,即這些swap也根本就沒有改變數組的任何排列,或者帶來任何變化。
版本2:有bug
void quick_sort(vector<int> &a,int l,int r)
{
if(l<r)
{
int po=a[l];
int ll=l,rr=r+1;
for(; ;)
{
while(a[++ll]<po);
while(a[--rr]>po);
if(ll>rr)
break;
else
swap1(a[ll],a[rr]);
}
swap1(a[l],a[rr]);
quick_sort(a,l,rr);
quick_sort(a,rr+1,r);
}
}
//來源:https://www.zhihu.com/question/24361443/answer/362777698
分析:這個版本也是參考的《數據結構與算法分析c++描述》,但改寫不成功,有兩個明顯的bug:
- bug1:如果輸入的數組a的第一個位置的元素是整個數組的最大值,那么,在
while(a[++ll]<po);
這句中,因為所有的元素都滿足while條件,那么ll必定會執行到數組的最后一位,直至越界報錯。 - bug2:如果輸入數組的第一個位置是整個數組的最小值,那么第二個while循環就會出現bug1同樣的錯誤,rr會一直往前走,直至越界報錯。
其實,這個bug,在網上很多版本中都存在這樣的問題。所以,在《數據結構與算法分析 c語言描述》中特意分析了哨兵設置或者邊界處理的細節。
版本3
template <typename T>
void quick_sort_recursive(T arr[], int start, int end) {
if (start >= end)
return;
T mid = arr[end];
int left = start, right = end - 1;
while (left < right) {
while (arr[left] < mid && left < right)
left++;
//考慮到數組的重復元素,因此這里用的是大於等於號,而不是大於號。
//如果使用大於號,那么當left和
while (arr[right] >= mid && left < right)
right--;
std::swap(arr[left], arr[right]);
}
if (arr[left] >= arr[end])
std::swap(arr[left], arr[end]); //left位置為mid
else
left++;
quick_sort_recursive(arr, start, left - 1);
quick_sort_recursive(arr, left + 1, end);
}
template <typename T> //整數或浮點數皆可使用,若要使用物件(class)時必須設定"小於"(<)、"大於"(>)、"不小於"(>=)的運算子功能
void quick_sort(T arr[], int len) {
quick_sort_recursive(arr, 0, len - 1);
}
//來源:https://zh.wikipedia.org/wiki/%E5%BF%AB%E9%80%9F%E6%8E%92%E5%BA%8F
分析:這個選取的樞紐元是數組的最后一個。這里應該注意的細節有:
- 在第二個while語句中,增加了
left < right
的條件。這個就是為了避免產生版本2中的越界bug - 在第三個while語句中,使用了大於等於號而不是大於,這個細節是為了解決重復元素問題,即當前left和right位置的數據如果都等於mid,那么
left++;
不會執行,如果下面使用大於的話,那么right--;
也不會執行,然后執行玩swap之后,left和right位置的值都還是等於mid,然后重復該步驟,即陷入死循環。但這這種做法的一個弊端是由於左右兩邊對等於pivot的值的處理方式不相同,容易造成不平衡的分割。 - 由於在第二個和第三個while中,都有
left < right
條件的限制,那么,當第一個while結束的時候,並不能保證left位置的數值大於等於mid,同樣,也不能保證right位置的數值小於mid。所以,在第一個while結束之后,會有一個if-else語句。對於這個if-else,首先可以肯定的是,來到這里之后肯定有left==right。如果是if條件,那么很好理解,執行完之后left位置為mid值,然后分別處理左右兩邊的。如果是else條件,我們考慮最后一次第一個while循環,記為W。可以肯定的是,在W中,swap的時候已經是left==right了,且要進入到else分之,那么就有left或者right位置的值都小於arr[end],即mid。因此,第二個while停止的條件肯定是left==right(因為arr[left]必須要小於mid)。於是,第三個while也因為left==right停止。即在W中,right的值沒有變化,那么說明前一個大循環(假設有,且記為W1)結束時就已經有arr[right]<mid.那么W1結束之后的狀態為left<right(因為后面還要執行W循環),且arr[right]<mid。這顯然是不能的,因為如果有最終left<right,那么第二個while肯定是因為arr[left]<mid不滿足(即arr[left]>=mid)而停止,然后經過swap之后,必定會有arr[right]>=mid.所以W是經歷的有且僅有的一次的大while循環。前面已經推出在W中,right值沒有變化。那么可以推出,僅僅經過循環W,即在第一次處理第二個while的時候,就已經實現了left==right,於是就可以推出,[left,right]的所有值都小於mid。而mid=arr[end],所以,如果進入了else,在left++;
之后,必定有arr[left]==mid=arr[end && left==end
成立。於是后面的就都可以理解了。 - 有些會在
quick_sort_recursive(arr, start, left - 1);
這里加一個if(left)
之類的,即避免left-1為復數。這個雖然不加也不會報錯,因為到遞歸調用時,if(start>=end)
肯定會滿足。但是我認為是有好處的。即通過一次if判斷而避免了一次遞歸調用。但這同時也會給其他情況下的遞歸多一次if判斷。所以,可以考慮把函數最開始的條件判斷放到這里來。只要在最初調用的時候進行參數檢查即可。
版本4:迭代法
// 參考:http://www.dutor.net/index.php/2011/04/recursive-iterative-quick-sort/
struct Range {
int start, end;
Range(int s = 0, int e = 0) {
start = s, end = e;
}
};
template <typename T> // 整數或浮點數皆可使用,若要使用物件(class)時必須設定"小於"(<)、"大於"(>)、"不小於"(>=)的運算子功能
void quick_sort(T arr[], const int len) {
if (len <= 0)
return; // 避免len等於負值時宣告堆疊陣列當機
// r[]模擬堆疊,p為數量,r[p++]為push,r[--p]為pop且取得元素
Range r[len];
int p = 0;
r[p++] = Range(0, len - 1);
while (p) {
Range range = r[--p];
if (range.start >= range.end)
continue;
T mid = arr[range.end];
int left = range.start, right = range.end - 1;
while (left < right) {
while (arr[left] < mid && left < right) left++;
while (arr[right] >= mid && left < right) right--;
std::swap(arr[left], arr[right]);
}
if (arr[left] >= arr[range.end])
std::swap(arr[left], arr[range.end]);
else
left++;
r[p++] = Range(range.start, left - 1);
r[p++] = Range(left + 1, range.end);
}
}
//來源:https://zh.wikipedia.org/wiki/%E5%BF%AB%E9%80%9F%E6%8E%92%E5%BA%8F
分析:
- 定義了一個處理范圍類型Range來表示每次處理的范圍
- 通過p值的變化,實現了一個類似棧的處理流程,即和函數遞歸的處理流程是一樣的
- 設置的棧的大小,即
Range r[len];
語句,即為數組的大小。因為當不斷迭代之后,每次迭代處理的范圍最少為1,所以只需要len就足夠了。除非極端情況,比如完全的逆序輸入,一般情況不需要len的空間
轉載請注明,原文來自https://www.cnblogs.com/willhua/p/9426130.html
版本5:來自《數據結構與算法分析 C語言描述》
#define LEN_LIMINT (3)
int median3(int A[], int left, int right)
{
int mid = (left + right) / 2;
if(A[left] > A[mid])
swap(A[left], A[mid]);
if(A[left] > A[right])
swap(A[left], A[right]);
if(A[mid] > A[right])
swap(A[mid], A[right]);
swap(A[mid], A[right-1]);
return A[mid];
}
void quicksort(int a[], int left, int right)
{
if(left + LEN_LIMINT < right)
{
int pivot = median3(a, left, right);
int ll = left, rr = right - 1;
while(ll < rr)
{
while(a[++ll] < pivot){}
while(a[--rr]) > pivot){}
if(ll < rr)
swap(a[ll], a[rr]);
else
break;
}
swap(a[ll], a[right - 1]);
quicksort(a, left, ll - 1);
quicksort(a, ll + 1, right);
}
else
{
//當數組較小的時候,使用插入排序更好
insertionsort(a + left, right - left + 1);
}
}
分析:
- 使用了median3方法來獲取pivot值,並且讓a[left]<=a[mid]<=a[right],這樣保證了在while中不會發生越界的情況。然后再把pivot放到了right-1的位置,這也會對ll起到警戒作用
- 由於上面提到的警戒作用的存在,因此不用像版本3一樣,還判斷left<right
- 由於在median3中就已經對left,mid,right排序,所以在while中可以使用前置符號,一開始就把ll和rr分別初始化為left+1和right-2
- 對於while循環,如果使用
while(a[ll]<pivot) ll++;while(a[rr]>pivot) rr--;
來替代,那么可能出現ll和rr位置都等於pivot的,那么就會死循環的情況。 - 對於LEN_LIMIT,從快排與插入排序的性質考慮,推薦的值是10。這樣比完全使用快排大概快15%。5到20之間都算合適。如果小於3,那么median3取三個值,卻實際上只取了一個或者兩個值,在細節上可能更加麻煩。
樞紐元pivot選擇
- 選擇第一個元素:如果輸入是隨機的,那么這種做法是可以接受,如果輸入是預排序的,那么這種做法就是不可接受的。因為這樣會導致劣質的分割,分割的兩邊極度不均衡,甚至分割相當於啥也沒干。在實際情況中,大部分時候都是預排序的,因此,這種做法應當放棄。還有一種類似的想法就是取兩個互異的關鍵字中大者,這種做法有同樣的壞處。
- 隨機選取樞紐:這種做法是安全的,但是問題在隨機的開銷都是昂貴的。
- 三數中值分割法:最理想的樞紐值應該是當前數組的中值,但是為了獲得這個中值都需要進行排序,開銷很大。所以,可以考慮隨機取數組中的三個值,把其中的中值作為樞紐元。但是,實際上,隨機選取並沒有太大的明顯意義,所以一般取數組的頭尾和中間位置的三個值,然后取其中值作為樞紐元,根據統計,這種做法大約會提升5%的速度,也是最為推薦的做法。