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,多一次比較。
總結:個人推薦單向掃描的優化版本,雙向掃描可以看到會有越界的問題,為了防止越界付出了一定代價。
