快速排序
快速排序引人注目的优点包括它是原地排序(只需要一个很小的辅助栈),且将长度为N 的数组排序所需的时间和NlgN 成正比。另外,快速排序的内循环比大多数排序算法都要短小,这意味着它无论是在理论上还是在实际中都要更快。
它的主要缺点是非常脆弱,在实现时要非常小心才能避免低劣的性能。
快速排序是一种分治的排序算法。它将一个数组切分(partition)成两个子数组,前一个数组所有元素均不大于后一个数组的任意元素。将两部分独立地排序。当两个子数组都有序时整个数组也就自然有序了。
我认为快速排序就是让每一个切分元素回到属于他的位置上!
快速排序的实现过程如算法2.5 所示。
// 算法2.5
public class Quick
{
public static void sort(Comparable[] a)
{
StdRandom.shuffle(a); // 这是算法4配套的函数包里的一个方法,
// 将数组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); // 将左半部分a[lo...j-1]排序
sort(a, j+1, hi); // 将右半部分a[j+1...hi]排序
}
}
快速排序的切分
切分(partition)这个方法是快速排序算法的关键方法,这个方法要使得数组a满足下面三个条件:
- 对于某个 j,a[ j ]已经排定;
- a[ lo ]到 a[ j-1 ]中的所有元素都 <= a[ j ];
- a[ j+1 ]到 a[ hi ]中的所有元素都 => a[ j ]。
我们就是通过递归地调用切分来排序的。
要想实现切分方法,一般策略是先随意地取a[ lo ] 作为切分元素,即那个将会被排定的元素,然后我们从数组的左端开始向右扫描直到找到一个大于等于它的元素,再从数组的右端开始向左扫描直到找到一个小于等于它的元素。这两个元素显然是没有排定的,因此我们交换它们的位置。如此继续,我们就可以保证左指针 i 的左侧元素都不大于切分元素,右指针 j 的右侧元素都不小于切分元素。当两个指针相遇时,我们只需要将切分元素a[ lo ] 和左子数组最右侧的元素(a[ j ])交换然后返回 j 即可。
快速排序的切分
// 快速排序的切分(partition)
private static int partition(Comparable[] a, int lo, int hi)
{ // 将数组切分为a[lo...i-1], a[i], a[i+1...hi]
int i = lo, j = hi+1; // 左右扫描指针
Comparable v = a[lo]; // 切分元素
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); // 当两个指针相遇时,将切分元素a[lo]和左子数组
// 最右侧的元素(a[j])交换
return j; // 然后返回 j 即可。
}
性能特点
将长度为N 的无重复数组排序,快速排序平均需要~2NlnN 次比较(以及1/6 的交换)。
快速排序最多需要约N2/2 次比较,但随机打乱数组能够预防这种情况。
总的来说,对于大小为N 的数组,算法2.5 的运行时间在1.39NlgN 的某个常数因子的范围之内。归并排序也能做到这一点,但是快速排序一般会更快(尽管它的比较次数多39%),因为它移动数据的次数更少。这些保证都来自于数学概率,你完全可以相信它。
三种算法改进
切换到插入排序
对于小数组,快速排序比插入排序慢,因为递归,快速排序的sort()方法在小数组中也会调用自己,因此在排序小数组时应该切换到插入排序。将sort()中的语句 if (hi <= lo) return; 替换成
if ( hi <= lo + M) { Insertion.sort(a, lo, hi); return; }即可实现对小数组使用插入排序。转换参数M 的最佳值是和系统相关的。
三取样切分
使用子数组的一小部分元素的中位数来切分数组,目的就是尽量让切分元素都能落在数组的中间,但代价是需要计算中位数。
熵最优的排序
这种情况主要针对数组中含有大量重复元素,一个简单的想法是将数组切分为三部分,分别对应小于、等于和大于切分元素的数组元素。
根据这一简单想法Dijkstra提出了三向切分的快速排序
代码格式如下:
// 三向切分的快速排序
public class Quick3way
{
private static void sort(Comparable[] a, int lo, int hi)
{ // 调用此方法的公有方法sort()请见算法2.5
if(hi <= lo)
{
return;
}
int lt = lo, i = lo+1, gt = hi;
Comparable v = a[lo]; // 我们使用Comparable 接口
//(而非less())对a[i] 进行三向比较
// 来直接处理以下情况:
while(i <= gt)
{ // 从左到右遍历数组依次,
// 维护一个指针 lt 使得:a[lo...lt-1] < v,
// 维护一个指针 gt 使得:a[gt+1...hi] > v,
// 维护一个指针 i 使得:a[lt...i-1] = v, a[i...gt]中的元素都还未确定
int cmp = a[i].compareTo(v);
if(cmp < 0)
{ // 如果a[i] < v,将a[lt]和 a[i]交换,将 lt和 i加一;
exch(a, lt++, i++);
}
else if(cmp > 0)
{ // 如果a[i] > v,将 a[gt]和 a[i]交换,将 gt减一;
exch(a, i, gt--);
}
else
{ // 如果a[i] = v,将 i 加一
i++;
}
} // 从左到右循环一遍后,
// 现在a[lo..lt-1] < v = a[lt..gt] < a[gt+1..hi]成立,如下图所示
sort(a, lo, lt-1);
sort(a, gt+1, hi);
}
}
这段排序代码的切分能够将和切分元素相等的元素归位,这样它们就不会被包含在递归调用处理的子数组之中了。对于存在大量重复元素的数组,这种方法比标准的快速排序的效率高得多。
没有最好的算法,只有在不同情况下存在最好的算法。