快速排序 partition函數的所有版本比較


partition函數是快排的核心部分

它的目的就是將數組划分為<=pivot和>pivot兩部分,或者是<pivot和>=pivot

其實現方法大體有兩種,單向掃描版本雙向掃描版本,但是具體到某個版本,其實現方法也是千差萬別,參差不齊。本着嚴謹治學的態度,我將目前所接觸的所有實現列舉出來,並作出比較。除了偽代碼,我也會給出相應的C&C++實現,供讀者參考。

單向掃描:

下面是算法導論中例子

PARTITION(A, p, r)
    x = A[r]
    i = p - 1
    for j = p to r - 1
        if A[j] <= x
            i = i + 1
            exchange A[i] with A[j]
    exchange A[i + 1] with A[r]
    return i + 1

int partition(int a[], int p, int r)
{
    int x = a[r];
    int i = p - 1;
    int j = p;
    for (; j < r; ++j)
        if (a[j] <= x)
            swap(&a[++i], &a[j]);
    swap(&a[i + 1], &a[j]);
    return i + 1;
}

這個是標准的單向掃描,其思路是:

將小於或等於pivot的元素通過交換全部移到前面去,這里需要注意的是i的作用,這是個哨兵,用於記錄交換后的位置,也就是i之前的元素都是交換好了的。

下面是一些可以變動的地方:

1.可以將小於pivot的元素移到前面去,而不是小於等於,這樣可以減少些交換次數,同理,可以將大於pivot的元素移到后面去,不過這樣就需要倒序遍歷了

2.或者是將i的初始值設置為p,而不是p-1;

3.可以將pivot設置成第一個元素;

4.存在i=j的情況,這時候的交換就是多余的,可以優化掉。

 

下面是稍作優化的版本

int partition(int a[], int p, int r)
{
    int x = a[r];
    int i = p;
    int j = p;
    for (; j < r; ++j)
        if (a[j] < x) {
            if (i != j)
                swap(&a[i], &a[j]);
            i++;
        }
    swap(&a[i], &a[j]);
    return i;
}

 

雙向掃描:

算法導論上的課后題有該算法,但是錯誤百出,這里以《算法》第四版的方法為例

PARTITION(A, p, r)
    x = A[p]
    i = p
    j = r + 1
    while true
        repeat
            j = j - 1
        until A[j] <= x
        repeat
            i = i + 1
        until A[i] >= x
        if i >= j
            break
        exchange A[i] with A[j]
    exchange A[p] with A[j]
    return j

int partition(int a[], int p, int r)
{
    int x = a[p];
    int i = p;
    int j = r + 1;
    while (true) {
        while (a[--j] > x);
        while (a[++i] < x);
        if (i >= j)
            break; 
        swap(&a[i], &a[j]);
    }
    swap(&a[j], &a[p]);
    return j;
}

其思路是從左到右找到大於等於pivot的元素,從右到左找到小於等於pivot的元素,然后將這兩個元素交換,直到左右掃描相遇,最后還要進行一次交換,將pivot調整到正確位置

這是上面程序的變種,看起來差別很大,不過原理是相同的

int partition(int a[], int p, int r)
{
    int x = a[p];
    int i = p + 1;
    int j = r;
    while (i <= j) {
        while (a[j] > x) j--;
        while (a[i] < x) i++;
        if (i >= j)
            break;
        swap(&a[i++], &a[j--]);
    }
    swap(&a[j], &a[p]);
    return j;
}

我們看一下它的掃描條件,一個是大於等於,一個是小於等於,也就是說左右掃描點存在都等於pivot的情況,這時候我們是不用交換的。根據互補原理,一個掃描點條件是大於等於,那么另一掃描點條件應該是互補條件小於,這樣兩個掃描點交換就不會出現交換相等元素的情況。

另外程序還存在着巨大的溢出漏洞,內層的while循環如:

while (a[i] < x) i++;

我們無法保證其不會越界,事實上,我經過測試,發現i的值一旦越界就不確定了,雖然都能保證i >= j的臨界條件,但我們還是應該盡量避免越界問題

可以在循環中加入越界條件

int partition(int a[], int p, int r)
{
    int x = a[p];
    int i = p;
    int j = r + 1;
    while (true) {
        while (i < j && a[--j] >= x);
        if (i >= j) break;
        while (i < j && a[++i] < x);
        if (i >= j) break; 
        swap(&a[i], &a[j]);
    }
    swap(&a[j], &a[p]);
    return j;
}

變種的防越界版如下

int partition(int a[], int p, int r)
{
    int x = a[p];
    int i = p + 1;
    int j = r;
    while (true) {
        while (i <= j && a[j] >= x) j--;
        if (i > j) break;
        while (i <= j && a[i] < x) i++;
        if (i > j) break;
        swap(&a[i++], &a[j--]);
    }
    swap(&a[j], &a[p]);
    return j;
}

 

左右掃描的版本還有很多,讓我們再來舉幾個例子

網上流傳比較廣的一個版本是下面這個

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

仔細觀察會發現,它與我們上面介紹的版本幾乎如出一轍,不同的是,它沒有使用swap交換元素,而是依次覆蓋,最后再把pivot歸位

具體過程可以參閱:http://blog.csdn.net/morewindows/article/details/6684558

算法的時間復雜度是O(n),但是為什么要寫成雙循環呢?我們完全可以把它改成單循環,代碼如下:

int partition(int a[], int p, int r)
{
    int x = a[p];
    int i = p + 1;
    int j = r;
    while (i <= j) {
        if (a[j] > x)
        {
            j--;
            continue;
        }
        if (a[i] < x)
        {
            i++;
            continue;
        }
        swap(&a[i++], &a[j--]);
    }
    swap(&a[j], &a[p]);
    return j;
}

但是,並不推薦這種做法,因為每次判斷i的時候,勢必會再次判斷j,多一次比較。

 

總結:個人推薦單向掃描的優化版本,雙向掃描可以看到會有越界的問題,為了防止越界付出了一定代價。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2026 CODEPRJ.COM