快速排序流行的原因是它实现简单、适用于各种不同的输入数据且一般应用中比其他排序算法都要快很多。快速排序引人注目的特点包括它是原地排序(只需要一个很小的辅助栈),且将长度为N的数组排序所需的时间和NlgN成正比。它的主要缺点是非常脆弱,在实现时要非常小心才能避免低劣的性能。
快速排序是一种分治的算法。它将一个数组分成两个子数组,将两部分独立地排序。在快速排序中,切分(partition)的位置取决于数组的内容。
一般策略是先随意地选取a[lo]作为切分元素,即那个将会被排定的元素,然后我们从数组的左端开始向右扫描直到找到一个大于等于它的元素,再从数组的右端开始向左扫描直到找到一个小于等于它的元素。这两个元素显然没有排定的,因此我们交换它们的位置。如此继续,我们就可以保证左指针i的左侧元素都不大于切分元素,右指针j的右侧元素都不小于切分元素。当两个指针相遇时,我们只需要将切分元素a[lo]和左子数组最右侧的元素(a[j])交换然后返回j即可。大致过程如图。
package section2_1;
import java.util.Random;
public class Quick {
private static void shuffle(Object[] a) {
int n = a.length;
Random random;
long seed = System.currentTimeMillis();
random = new Random(seed);
for (int i = 0; i < n; i++) {
int r = i + random.nextInt(n-i); // between i and n-1
Object temp = a[i];
a[i] = a[r];
a[r] = temp;
}
}
private static boolean less(Comparable v, Comparable w) {
return v.compareTo(w) < 0;
}
private static void exch(Comparable[] a, int i, int j) {
Comparable t = a[i];
a[i] = a[j];
a[j] = t;
}
private static void show(Comparable[] a) {
for (int i=0;i<a.length;i++) {
System.out.print(a[i]+" ");
}
System.out.println();
}
public static boolean isSorted(Comparable[] a) {
for (int i=1;i<a.length;i++) {
if (less(a[i],a[i-1])) return false;
}
return true;
}
public static void sort(Comparable[] a) {
shuffle(a);
sort(a,0,a.length-1);
}
private static void sort(Comparable[] a, int lo,int hi) {
if (lo>=hi) 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) {
Comparable v = a[lo];
int i = lo;
int j = hi + 1;
while (true) {
while (less(a[++i],v)) if (i == hi) break;
while (less(v,a[--j])) if (j == lo) break;
if (i>=j) break;
exch(a,i,j);
}
exch(a,lo,j);
return j;
}
public static void main(String[] args) {
String[] a = {"K","R","A","T","E","L","E","P","U","I","M","Q","C","X","O","S"};
sort(a);
assert isSorted(a);
show(a);
}
}
切分轨迹:
排序轨迹:
性能分析
快速排序切分方法的内循环会用一个递增的索引将数组元素和一个定值比较。这种简洁性也是快速排序的一个优点,很难想象排序算法中还能有比这更短小的内循环了。
快速排序另一个速度优势在于它的比较次数很少。排序效率最终还是依赖切分数组的效果,而这依赖于切分元素的值。
快速排序的最好情况是每次都正好能将数组对半分。
它的基本实现仍有一个潜在的缺点:在切分不平衡时这个程序可能会极为低效。例如,第一次从最小元素切分,第二次从第二小的元素切分,如此这般,每次调用只会移除一个元素。我们要在快速排序前将数组随机排序的主要原因就是要避免这种情况。
算法改进
切换到插入排序
和大多数递归排序算法一样,改进快速排序性能的一个简单办法基于以下两点:
- 对于小数组,快速排序比插入排序慢;
- 因为递归,快速排序的sort()方法在小数组中也会调用自己。
因此,在排序小数组时应该切换到插入排序。
package section2_1;
import java.util.Random;
public class QuickInsert {
private static final int CUTOFF = 8;
private static void shuffle(Object[] a) {
int n = a.length;
Random random;
long seed = System.currentTimeMillis();
random = new Random(seed);
for (int i = 0; i < n; i++) {
int r = i + random.nextInt(n-i); // between i and n-1
Object temp = a[i];
a[i] = a[r];
a[r] = temp;
}
}
private static boolean less(Comparable v, Comparable w) {
return v.compareTo(w) < 0;
}
private static void exch(Comparable[] a, int i, int j) {
Comparable t = a[i];
a[i] = a[j];
a[j] = t;
}
private static void show(Comparable[] a) {
for (int i=0;i<a.length;i++) {
System.out.print(a[i]+" ");
}
System.out.println();
}
public static boolean isSorted(Comparable[] a) {
for (int i=1;i<a.length;i++) {
if (less(a[i],a[i-1])) return false;
}
return true;
}
public static void sort(Comparable[] a) {
shuffle(a);
sort(a,0,a.length-1);
}
private static void sort(Comparable[] a, int lo,int hi) {
int n = hi - lo + 1;
if (n <= CUTOFF) {
insertionSort(a, lo, hi);
return;
}
int j = partition(a,lo,hi);
sort(a,lo,j-1);
sort(a,j+1,hi);
}
private static void insertionSort(Comparable[] a, int lo, int hi) {
for (int i = lo; i <= hi; i++)
for (int j = i; j > lo && less(a[j], a[j-1]); j--)
exch(a, j, j-1);
}
private static int partition(Comparable[] a,int lo,int hi) {
Comparable v = a[lo];
int i = lo;
int j = hi + 1;
while (true) {
while (less(a[++i],v)) if (i == hi) break;
while (less(v,a[--j])) if (j == lo) break;
if (i>=j) break;
exch(a,i,j);
}
exch(a,lo,j);
return j;
}
public static void main(String[] args) {
String[] a = {"K","R","A","T","E","L","E","P","U","I","M","Q","C","X","O","S"};
sort(a);
assert isSorted(a);
show(a);
}
}
三取样切分
改进快速排序性能的第二个办法是使用子数组的一小部分元素的中位数来切分数组。这样做得到的切分更好,但代价是需要计算中位数。人们发现将取样大小设为3并用大小居中的元素切分的效果最好。
package section2_1;
import java.util.Random;
public class QuickMedian3 {
private static void shuffle(Object[] a) {
int n = a.length;
Random random;
long seed = System.currentTimeMillis();
random = new Random(seed);
for (int i = 0; i < n; i++) {
int r = i + random.nextInt(n-i); // between i and n-1
Object temp = a[i];
a[i] = a[r];
a[r] = temp;
}
}
private static boolean less(Comparable v, Comparable w) {
return v.compareTo(w) < 0;
}
private static void exch(Comparable[] a, int i, int j) {
Comparable t = a[i];
a[i] = a[j];
a[j] = t;
}
private static void show(Comparable[] a) {
for (int i=0;i<a.length;i++) {
System.out.print(a[i]+" ");
}
System.out.println();
}
public static boolean isSorted(Comparable[] a) {
for (int i=1;i<a.length;i++) {
if (less(a[i],a[i-1])) return false;
}
return true;
}
public static void sort(Comparable[] a) {
shuffle(a);
sort(a,0,a.length-1);
}
private static void sort(Comparable[] a, int lo,int hi) {
if (lo>=hi) return;
int n = hi - lo + 1;
int m = median3(a, lo, lo + n/2, hi);
exch(a, m, lo);
int j = partition(a,lo,hi);
sort(a,lo,j-1);
sort(a,j+1,hi);
}
private static int median3(Comparable[] a, int i, int j, int k) {
return (less(a[i], a[j]) ?
(less(a[j], a[k]) ? j : less(a[i], a[k]) ? k : i) :
(less(a[k], a[j]) ? j : less(a[k], a[i]) ? k : i));
}
private static int partition(Comparable[] a,int lo,int hi) {
Comparable v = a[lo];
int i = lo;
int j = hi + 1;
while (true) {
while (less(a[++i],v)) if (i == hi) break;
while (less(v,a[--j])) if (j == lo) break;
if (i>=j) break;
exch(a,i,j);
}
exch(a,lo,j);
return j;
}
public static void main(String[] args) {
String[] a = {"K","R","A","T","E","L","E","P","U","I","M","Q","C","X","O","S"};
sort(a);
assert isSorted(a);
show(a);
}
}
熵最优的排序
实际应用中经常会出现含有大量重复元素的数组。在有大量重复元素的情况下,快速排序的递归性会使元素全部重复的子数组经常出现,这就有很大的改进潜力。
一个简单的想法是将数组切分为三部分,分别对应小于、等于和大于切分元素的数组元素。
三向切分的快速排序:
它从左到右遍历数组一次,维护一个指针lt使得a[lo…lt-1]中的元素都小于v,一个指针gt使得a[gt+1…hi]中的元素都大于v,一个指针i使得a[lt…i-1]中的元素都等于v,a[i…gt]中的元素都还未确定,如图所示:
我们使用Comparable接口对a[i]进行三向比较来直接处理以下情况:
- a[i]小于v,将a[lt]和a[i]交换,将lt和i加一;
- a[i]大于v,将a[gt]和a[i]交换,将gt减一;
- a[i]等于v,将i加一。
package section2_1;
import java.util.Random;
public class Quick3way {
private static void shuffle(Object[] a) {
int n = a.length;
Random random;
long seed = System.currentTimeMillis();
random = new Random(seed);
for (int i = 0; i < n; i++) {
int r = i + random.nextInt(n-i); // between i and n-1
Object temp = a[i];
a[i] = a[r];
a[r] = temp;
}
}
private static boolean less(Comparable v, Comparable w) {
return v.compareTo(w) < 0;
}
private static void exch(Comparable[] a, int i, int j) {
Comparable t = a[i];
a[i] = a[j];
a[j] = t;
}
private static void show(Comparable[] a) {
for (int i=0;i<a.length;i++) {
System.out.print(a[i]+" ");
}
System.out.println();
}
public static boolean isSorted(Comparable[] a) {
for (int i=1;i<a.length;i++) {
if (less(a[i],a[i-1])) return false;
}
return true;
}
public static void sort(Comparable[] a) {
shuffle(a);
sort(a,0,a.length-1);
}
private static void sort(Comparable[] a, int lo,int hi) {
if (lo>=hi) return;
int lt = lo;
int i = lo + 1;
int gt = hi;
Comparable v = a[lo];
while (i <= gt) {
int cmp = a[i].compareTo(v);
if (cmp < 0) exch(a,lt++,i++);
else if (cmp > 0) exch(a,i,gt--);
else i++;
}
//现在a[lo..lt-1]<v=a[lt..gt]<a[gt+1..hi]成立
sort(a,lo,lt-1);
sort(a,gt+1,hi);
}
public static void main(String[] args) {
String[] a = {"R","B","W","W","R","W","B","R","R","W","B","R"};
sort(a);
assert isSorted(a);
show(a);
}
}
三向切分的快速排序的轨迹如图:
该算法能够将和切分元素相等的元素归位,这样它们就不会被包含在递归调用处理的子数组之中了。对于存在大量重复元素的数组,这种方法比标准的快速排序的效率高得多。
对于只有若干不同主键的随机数组,归并排序的时间复杂度是线性对数的,而三向切分快速排序则是线性的。
三向切分的最坏情况正是所有主键均不同。当存在重复主键时,它的性能就会比归并排序好很多。
对于包含大量重复元素的数组,它将排序时间从线性对数降低到了线性级别。这种对重复元素的适应性使得三向切分的快速排序成为排序库函数的最佳算法选择,因为需要将包含大量重复元素的数组排序的用例很常见。