1. 程式人生 > >快速排序(快排)的一些細節和k-th問題

快速排序(快排)的一些細節和k-th問題

對演算法競賽而言,軸點的選取不是關鍵,演算法的細節和程式才是重點,而在應用快排的副產品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個元素,使得時間複雜度變成O

(n2)。而如果用嚴格大於/小於,雖然兩邊指標每此只挪動1位,但是它們會在正中間相遇,產生一個最好的劃分,時間複雜度為log2n

另一個因素是,如果將樞紐元放在陣列兩端,用嚴格大於/小於就可以將樞紐元作為一個哨兵元素,從而減少內層迴圈的一個測試。
由以上兩點,內層迴圈中的while測試一般用“嚴格大於/小於”。

這個演算法的妙處在於第14行放置x的值,由於前面剛好劃分出兩段,那麼x剛好位於第ij處,這樣第4行,外層迴圈的條件也就不能取“=”號。這也是應用於k-th問題的一個依據。

“兩頭”交換(這應該是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問題,簡單的指將所有元素非降排序後,位於第k位的元素。由於相同元素的存在,第k位的元素,不一定是第k小(大)的元素,但是簡化後的問題應該沒有疑議,處理起來也簡單點。

標準寫法的演化版

由標準寫法的第14行可知,若此時i=k,那麼剛剛可以接受查詢;否則,k<i那麼只需在前半段裡找即可;又否則,k>i那麼只需在後半段裡找即可。

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;
}