快速排序
流行原因
- 实现简单
- 适用于各种不同的输入数据
- 在一般应用中比其他的排序算法要快得多
显著特点
- 省空间: 原地排序(只需要一个很小的辅助栈)
- 省时间: 长度为 N N N的数组排序所需时间和 N l o g 2 N Nlog_2N Nlog2N成正比
- 将长度为 N N N的无重复数组排序,快速排序平均需要 2 N l n N ~2NlnN 2NlnN次比较(以及 1 / 6 1/6 1/6的交换)
- 快速排序比归并排序一般更快(尽管它的比较次数多39%),因为它移动数据的次数更少
理想状态
每次都正好能将数组对半分。此时比较次数正好满足分治递归的 C N = 2 C N / 2 + N C_N = 2C_{N/2} + N CN=2CN/2+N 。
2 C N / 2 2C_{N/2} 2CN/2 标识两个子数组排序的成本, N N N 标识用切分元素和所有数组元素比较的成本。
潜在缺点
在切分不平衡时这个程序可能极为低效。例如,如果第一次从最小的元素切分,第二次从第二小的元素切分,如此这般,每次调用只会移除一个元素。这会导致一个大子数组需要起分很多次。我门在快速排序前将数组随机排序的主要原因就是需要避免这种情况。
- 快速排序最多需要约 N 2 / 2 N^2/2 N2/2 次比较,但随机打乱数组能够预防这种情况
一般策略
- 随意取 a [ l o ] a[lo] a[lo] 作为***切分元素***
- 然后从左至右找到一个***大于等于***它的元素
- 再从右至左找到一个***小于等于***它的元素
- 当左( i i i)右指针( j j j)没有相遇时,这两个元素显然是没有排定的,因此交换他们的位置
- 然后继续刚才的操作,直至左右指针相遇
- 将切分元素 a [ l o ] a[lo] a[lo] 与左子数组最右侧的元素( a [ j ] a[j] a[j]) 交换位置,然后返回 j j j, 即为完成一次切分;此时 a [ j ] a[j] a[j]位置已经被排定,并且 j j j左边的元素一定不大于 a [ j ] a[j] a[j],右边的元素一定不小于 a [ j ] a[j] a[j]
- 然后分别递归的切分 a [ l o ] . . . a [ j − 1 ] a[lo]...a[j-1] a[lo]...a[j−1] 与 a [ j + 1 ] . . . a [ h i ] a[j+1]...a[hi] a[j+1]...a[hi] 即完成了对数组的排序
public class Quick {
public static void sort(Comparable[] a) {
// 打乱数组,避免极端情况;
// 例如,如果第一次从最小的元素切分,第二次从第二小的元素切分,
// 如此这般,每次调用只会移除一个元素。这会导致一个大子数组需要起分很多次
StdRandom.shuffle(a);
int length = a.length;
int hi = length - 1;
sort(a, 0, hi);
}
public 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;
// 右扫描指针
int 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;
}
// 将 找到的元素交换位置
exchange(a, i, j);
}
// 交换
exchange(a, lo, j);
return j;
}
public static boolean less(Comparable a, Comparable b) {
return a.compareTo(b) < 0;
}
public static void exchange(Comparable[] a, int i, int j) {
Comparable temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
算法改进
-
切换到插入排序
和大多数递归排序算法一样,改进快速排序性能的一个简单办法是一下两点:
- 对于小数组,快速排序比插入排序慢
- 因为递归,快速排序的 sort() 方法在小数组中也会调用自己
public static void sort(Comparable[] a, int lo, int hi) { // 转换参数m的最佳值和系统相关, // 但 5 ~ 15 之间的任意值在大多数情况下都能令人满意 int m = 10; if (hi <= lo + m) { Insertion.sort(a, lo, hi); return; } int j = partition(a, lo, hi); sort(a, lo, j - 1); sort(a, j + 1, hi); }
-
三取样切分
-
原理:使用子数组的小部分元素的中位数来切分数组
-
代价:需要计算中位数
-
取样大小:人们发现将取样大小设为 3 并用大小剧中的元素切分效果更好; 即随机取三个元素取中位数作为切分元素
-
还可以做:将取样元素放在数组的末尾作为“哨兵”来去掉 partition() 中的数组边界判断
public static void sort(Comparable[] a, int lo, int hi) { if (hi <= lo) { return; } dealPivot(a, lo, hi); int j = partition(a, lo, hi); sort(a, lo, j - 1); sort(a, j + 1, hi); } private static void dealPivot(Comparable[] a, int lo, int hi) { // 我直接使用 数组位置 第一个 中间的 最后一个 作为取样 int mid = lo + (hi - lo) / 2; if (less(a[mid], a[lo])) { exchange(a, lo, mid); } if (less(a[hi], a[lo])) { exchange(a, lo, hi); } if (less(a[hi], a[mid])) { exchange(a, mid, hi); } // 将切分元素放置到数组首位 exchange(a, lo, mid); }
-