八大排序演算法(五)——快速排序
快速排序可能是應用最廣泛的排序演算法。快速排序流行的原因是因為它實現簡單、適用於各種不同的輸入資料且在一般應用中比其他排序演算法都要快的多。快速排序的特點包括它是原地排序(只需要一個很小的輔助棧),且將長度為n的陣列排序所需的時間和nlogn成正比。快速排序的內迴圈比大多數排序演算法都要短小,這意味著無論是理論上還是實際上都要更快。它的主要缺點是非常脆弱,在實現時要非常小心才能避免低劣的效能,許多錯誤都致使它在實際中的效能只有平方級別。
5.1 基本演算法
快速排序是一種分治的排序演算法。它將一個數組分成兩個子陣列,將兩部分獨立地排序。快速排序和歸併排序是互補的:歸併排序將陣列分成兩個子陣列分別排序,並將有序的子陣列歸併以將整個陣列排序;而快速排序將陣列排序的方式則是當兩個子陣列都有序時整個陣列也就自然有序了。在第一種情況中,遞迴呼叫發生在處理整個陣列之前;在第二種情況之中,遞迴呼叫發生在處理整個陣列之後。在歸併排序中,一個數組被等分切成兩半;在快速排序中,切分的位置取決於陣列的內容。
public class QuickSort { public static void sort(Comparable[] a) { sort(a, 0, a.length - 1); } private static void sort(Comparable[] a, int lo, int hi) { if (hi <= lo) { return; } int j = partition(a, lo, hi); sort(a, lo, j - 1); sort(a, j + 1, hi); } private static int partition(Comparable[] a, int lo, int hi) { int i = lo, j = hi + 1; Comparable v = a[lo]; while() { while (a[++i] < v) { if (j == hi) { break; } } while (v < a[--j]) { if (j == lo) { break; } } if (i >= j) { break; } swap(a, i, j); } swap(a, lo, j); return j; } }
5.1.1 原地切分
如果使用一個輔助陣列,可以很容易地實現切分,但產生了將切分後的陣列複製回去的開銷。將空陣列放在遞迴的切分方法中,這會極大地降低排序效率。
5.1.2 別越界
如果切分元素是陣列中最大或最小的元素,就要小心別讓掃描指標跑出陣列的邊界。partition()實現可明確進行明確的檢測來預防這種情況。
5.1.3 保持隨機性
在partition()中隨機選擇一個切分元素。
5.1.4 切分元素有重複
左側掃描最好是在遇到大於等於切分元素時停下,右側掃描則是遇到小於等於切分元素時停下。儘管這樣可能會造成一些不必要的等值交換,但在某些典型應用中,它能避免演算法的執行時間變成平方級別。
5.1.5 終止遞迴
需要保證遞迴總是能夠結束。
5.2 效能特點
快速排序切分方法的內迴圈會用一個遞增的索引將陣列元素和一個定值比較。這種簡潔些是快排的一個優點,排序演算法中很難還能有比這更小的內迴圈了。
快排另一個速度優勢在於它的比較次數很少。排序效率最終還是依賴切分陣列的效果,而這依賴於切分元素的值。
快速排序的最好情況是每次都能正好將陣列對半分。
將長度為n的無重複陣列排序,快速排序平均需要~2nlogn此比較(以及1/6的交換)。
儘管快速排序有很多優點,它的基本實現仍有一個缺點:在切分不平衡是這個程式可能會極為低效。
快速排序最多需要約n^2/2次比較,但隨機打亂陣列能夠預防這種情況。
總的來說,可以肯定的是對於大小為n的陣列,此演算法的執行時間在1.39nlogn的某個因子的範圍之內。歸併排序也能做到這一點,但快速排序一般會更快,因為它移動資料的次數更少。
5.3 演算法改進
5.3.1 切換到插入排序
基於以下兩點:對於小陣列,快速排序比插入排序慢;因為遞迴,快速排序的sort()在小陣列中也會呼叫自己。
5.3.2 三切分取樣
使用子陣列的一小部分元素的中位數來切分陣列。這樣做的切分效果更好,但是需要計算中位數。人們發現將取樣大小設為3並用大小居中的元素切分效果最好。還可以將取樣元素放在陣列末尾作為“哨兵”來去掉partition()中的陣列邊界測試。
5.3.3 熵最優的排序
在有大量元素的情況下,快速排序的遞迴性會使元素全部重複的子陣列經常出現,這就有很大的改進潛力,將當前實現的線性對數級的效能提高到線性級。
一個簡單的想法就是將陣列切為3部分,分別對應小於、等於、大於切分元素的陣列元素。程式碼如下:
public class Quick3way {
private static void sort(Comparable[] a, int lo, int hi) {
if (hi <= lo) {
return;
}
int lt = lo, i = lo + 1, gt = hi;
Comparable v = a[lo];
while (i <= gt) {
int cmp = a[i].compareTo(v);
if (cmp > 0) {
swap(a, lt++, i++)
} else if (cmp > 0) {
swap(a, i, gt--)
} else {
i++;
}
}
sort(a, lo, lt - 1);
sort(a, gt + 1, hi);
}
}
三向切分的最壞情況正是所有元件均不相同。當存在重複元件時,它的效能會比歸併排序好得多。三向切分是資訊量最優的,即對於任意分佈的輸入,最優的基於比較的演算法平均所需的比較次數和三向切分的快速排序平均所需的比較次數相互處於常數因子範圍內。
對於標準的快速排序,隨著陣列規模的增大其執行時間會趨於平均執行時間,大幅偏離的情況是很少見的,因此可以肯定三向切分的快速排序執行時間和輸入的資訊量的N倍是成正比的。在實際應用中這個性質很重要,因為對於包含大量重複元素的陣列,它將排序時間從線性對數級降到對數級。