快速排序實現及其pivot的選取
coursera上斯坦福的算法專項在講到快速排序時,稱其為最優雅的算法之一。快速排序確實是一種比較有效的排序算法,很多類庫中也都采用了這種排序算法,其最壞時間復雜度為$O(n^2)$,平均時間復雜度為$O(nlogn)$,且其不需要額外的存儲空間。
基本步驟
快速排序主要使用了分治的思想,通過選取一個pivot,將一個數組劃分為兩個子數組。其步驟為:
1.從數組中選擇一個元素作為pivot
2.重新排列數組,小於pivot的在pivot的左邊,大於pivot的在其右邊。
3.遞歸地對劃分後的左右兩部分重復上述步驟。
簡單的偽代碼如下:
其中最主要的就是partition劃分過程了。
劃分過程
partition過程需要首先選擇一個pivot,然後將小於pivot的元素放到左半部分,大於pivot的放到右半部分,並且最終pivot的位置及為其在排序好的數組中的最終位置。
這裏使用第一個元素作為pivot,若選擇其他元素作為pivot,則將其交換到第一個元素,這樣可以保證代碼的一致性及容易實現。示意圖如下:
這裏使用i和j,i和j最初為p+1的位置,在遍歷的過程中i始終指向>p的第一個元素,j始終指向當前待遍歷的元素,若a[j] < p,則將其與a[i]進行交換。相關過程如下:
基本實現如下:
/** * a[l+1],...,a[i-1] < p * a[i],...,a[j-1] > p */ private static int partition(int[] a, int l, int r) { int p = a[l]; int i = l + 1; for (int j=l+1; j<=r; j++) { if (a[j] < p) { swap(a, j, i); i++; } } swap(a, l, i-1); return i-1; }
基本實現
public class QuickSort { public static void qSort(int[] a) { if (a == null || a.length <= 1) { return; } qSort(a, 0, a.length-1); } private static void qSort(int[] a, int l, int r) { if (l >= r) { return; } int pos = partition(a, l, r); qSort(a, l, pos - 1); qSort(a, pos + 1, r); } /** * a[l+1],...,a[i-1] < p * a[i],...,a[j-1] > p */ private static int partition(int[] a, int l, int r) { int p = a[l]; int i = l + 1; for (int j=l+1; j<=r; j++) { if (a[j] < p) { swap(a, j, i); i++; } } swap(a, l, i-1); return i-1; } //返回pivot下標 選擇第一個元素 private static int choosePivotFirst(int[] a, int l, int r) { return l; } private static void swap(int[] a, int x, int y) { int temp = a[x]; a[x] = a[y]; a[y] = temp; }
pivot的選取
根據斯坦福算法專項課,然我們實現三種不同的pivot選取方式,並計算相應比較次數,分別為choose first, choose last, median of three, 還可以進行隨機選取,這也是快速排序為什麽是一種隨機化算法。
pivot的選取決定了快速排序的運行時間,下面對幾種特殊情況進行分析:
1.最壞情況
假設我們始終選取第一個元素作為pivot, 並且輸入數組是有序的,那麽每次劃分後面所有元素都大於pivot, 每次只能將問題規模減少1,所以運行時間為$n+n-1+n-2+...+1$ = $O(n^2)$.
2.最好情況
最好情況為每次選取的pivot都能將數組平均地劃分為兩部分,由於劃分的過程為$O(n)$,所以總的運行時間為$$T(n) = 2T(n/2) + O(n)$$根據主方法,時間復雜度為O(nlogn)。
3.隨機選取
每次運行過程中,隨機選取pivot, 通常能得到比較好的結果。
選取方式及實現
斯坦福算法專項課上讓我們實現三種不同的選取方式,選取第一個,最後一個,以及三數取中。
1.choose first
該種方式最為簡單,只需返回子數組的第一個元素下標即可,下面為其實現:
//返回pivot下標 選擇第一個元素
private static int choosePivotFirst(int[] a, int l, int r) {
return l;
}
2.choose last
選擇最後一個元素,實現如下:
//選擇最後一個元素作為pivot
private static int choosePivotLast(int[] a, int l, int r) {
return r;
}
3.median-of-three
選取第一個、最後一個以及中間的元素的中位數,如4 5 6 7, 第一個4, 最後一個7, 中間的為5, 這三個數的中位數為5, 所以選擇5作為pivot,8 2 5 4 7, 三個元素分別為8 5 7, 中位數為7, 所以選擇最後一個元素7作為pivot,其實現如下:
//median-of-three pivot rule
private static int choosePivotMedianOfThree(int[] a, int l, int r) {
int mid = 0;
if ((r-l+1) % 2 == 0) {
mid = l + (r-l+1)/2 - 1;
} else {
mid = l + (r-l+1)/2;
}
//只需要找出中位數即可,不需要交換
//有的版本也可以進行交換
if (((a[l]-a[mid]) * (a[l]-a[r])) <= 0) {
return l;
} else if (((a[mid]-a[l]) * (a[mid]-a[r])) <= 0) {
return mid;
} else {
return r;
}
}
最後的劃分過程如下:
private static int partition(int[] a, int l, int r) {
//pivot選擇方式
//int pi = choosePivotFirst(a, l, r);
//int pi = choosePivotLast(a, l, r);
int pi = choosePivotMedianOfThree(a, l, r);
//始終將第一個元素作為pivot, 若不是, 則與之交換
if (pi != l) {
swap(a, pi, l);
}
int p = a[l];
int i = l + 1;
for (int j=l+1; j<=r; j++) {
if (a[j] < p) {
swap(a, j, i);
i++;
}
}
swap(a, l, i-1);
return i-1;
}
註意最後的劃分過程相比於之前增加的pivot的選取方式,而不是單純地將第一個元素作為pivot, 可以看到,若第一個元素不是pivot, 需要將pivot與第一個元素進行交換,這樣保證代碼的統一性。
總結與感想
1.學會體會這些算法背後的思想,為什麽要這樣設計
2.對於比較復雜的算法,學會使用特殊情況進行分析
參考資料:
(1) coursera斯坦福算法專項課part1
(2) 維基百科快速排序
快速排序實現及其pivot的選取