快速排序(快排)的一些細節和k-th問題
阿新 • • 發佈:2018-12-30
對演算法競賽而言,軸點的選取不是關鍵,演算法的細節和程式才是重點,而在應用快排的副產品k-th元素問題中,這個細節尤為重要。網路上鮮有這些細節描述,謹以記之。
快排的不同寫法
主要用兩種寫法:標準快排和“兩頭”交換寫法,競賽中以後者居多。
標準寫法
void quick_sort(int l, int r)
{
int i = l, j = r, x = s[l];
while (i < j)
{
while(i < j && s[j] > x) j--;
if(i < j)
s[i++] = s[j];
while (i < j && s[i] < x) i++;
if(i < j)
s[j--] = s[i];
}
s[i] = x;
if (l<i) quick_sort(l, i - 1);
if (r>i) quick_sort(i + 1, r);
}
第6行內層迴圈中的while測試是用“嚴格大於/小於”還是”大於等於/小於等於”。
一般的想法是用大於等於/小於等於,忽略與樞紐元相同的元素,這樣可以減少不必要的交換,因為這些元素無論放在哪一邊都是一樣的。但是如果遇到所有元素都一樣的情況,這種方法每次都會產生最壞的劃分,也就是一邊1個元素,令一邊n-1個元素,使得時間複雜度變成 (n2)
另一個因素是,如果將樞紐元放在陣列兩端,用嚴格大於/小於就可以將樞紐元作為一個哨兵元素,從而減少內層迴圈的一個測試。
由以上兩點,內層迴圈中的while測試一般用“嚴格大於/小於”。
這個演算法的妙處在於第14行放置
“兩頭”交換(這應該是Hoare提出的最早的快排劃分法,算導說的)
void sort(int left, int right) {
int i = left, j = right, x = a[(i+j)>>1], tmp;
while (i<=j) {
while (a[i] < x) i++;
while (a[j] > x) j--;
if (i<=j){
tmp = a[i];
a[i] = a[j];
a[j] = tmp;
i++;
j--;
}
}
if (left<j) sort(left, j);
if (right>i) sort(i, right);
}
- 對於兩頭交換法,每次可以交換兩個數到正確區段,似乎效率更高,但是實際上,效率並不比標準演算法高
- 第3行迴圈的條件一般要取“=”,即指向同一元素時再比一次,以便分成兩段
- 第6行交換的條件必須取“=”,以便分成兩段
- 倘若第3行取了“=”,而第6行沒有取“=”,此時while將會造成死迴圈
- 對於第4、5行的
i,j 的移動來說,條件中不能取“=”。若軸點剛好是序列的最大值,那麼i,j 的值將會下標越界
k-th問題
這裡的k-th問題,簡單的指將所有元素非降排序後,位於第
標準寫法的演化版
由標準寫法的第14行可知,若此時
int findKth(int left, int right)
{
int i = left, j = right, x = s[left];
while (i < j)
{
while(i < j && s[j] > x) j--;
if(i < j)
s[i++] = s[j];
while(i < j && s[i] < x) i++;
if(i < j)
s[j--] = s[i];
}
s[i] = x;
if (k==i) return s[i];
if (left<i && k<i) return findKth(left, i - 1);
if (right>i && k>i) return findKth(i + 1, right);
}
兩頭交換的演化版
這個版本的軸點元素可能並不一定在原先位置,因此要迴圈到區間內只有一個元素為止。
int findKth(int left, int right) {
if (left == right) return a[left];
int i = left, j = right, x = a[(i+j)>>1], tmp;
while (i<=j) {
while (a[i] < x) i++;
while (a[j] > x) j--;
if (i<=j){
tmp = a[i];
a[i] = a[j];
a[j] = tmp;
i++;
j--;
}
}
if (left<=j && k<=j) return findKth(left, j);
if (right>=i && k>=i) return findKth(i, right);
return x;
}