##快速排序
采用分治思想divide-and-conquer进行排序,将数组分成两部分,并分别排序。
切分过程是快排的关键,切分使数组满足如下三个条件:
- The entry a[j] is in its final place in the array, for some j.对某个j,a[j]被排到最终位置上。
- No entry in a[lo] through a[j-1] is greater than a[j].a[lo] 到 a[j-1] 都不大于 a[j]
- No entry in a[j+1] through a[hi] is less than a[j].a[j+1] 到 a[hi] 都不小于 a[a]
然后递归地调用切分过程,将子数组排序。
###切分过程
可以选择 a[lo] 作为切分元素 v(切分完成时被排定)。然后从左端开始扫描,直到大于等于切分元素 v ,从右端开始扫描,直到小于等于切分元素 v。
内循环结束时,两个索引元素处在相反的两个子数组中,交换之。
如果两个索引交叉,意味着整个数组已遍历完,此时 j 指向左子数组最右,i 指向右子数组最左,j即是切分元素的最终位置,交换 a[lo] 和 a[j],并返回 j,完成切分。
###实现细节
- 使用原地排序
使用辅助数组,使切分的实现更简单,但把元素复制回原数组的开销也很可观。 - 注意不要越界
如果最小或最大的元素作为切分元素,注意不要越界。 - 保持随机性
通过shuffle将数组打乱,来保证快排的性能。也可以随机选择一个元素作为切分元素。 - 适时终止循环
尤其是含有与切分元素的值相同的元素的情况。 - 处理切分元素值重复
左侧循环最好在遇到大于等于切分元素时停止,右侧循环最好在遇到小于等于切分元素时停止。尽管可能会不必要地移动与切分元素相同值的元素,但可以避免算法消耗平方级的时间。 - 适时终止递归
尤其是切分元素恰好是最大或最小值时,避免陷入无尽的递归循环中。
###实现
当指针 i 和 j 相遇时主循环退出。在循环中,当 a[i] 小于 v 时我们增大 i。a[j] 大于 v 时我们减小 j,然后交换 a[i] 和 a[j] 来保证 i 左侧的元素都不大于 v,j 右侧的元素都不小于 v。当指针相遇时,交换 a[low] 和 a[j] ,切分值就留在 a[j] 中了,切分结束。
public class Quick extends Sort {
@Override
public void sort(Comparable[] a) {
if (a == null || a.length < 2) {
return;
}
shuffle(a);
sort(a, 0, a.length - 1);
assert isSorted(a);
}
private void sort(Comparable[] a, int low, int high) {
if (low >= high) {
return;
}
int j = partition(a, low, high);// 切分
sort(a, low, j - 1);// 左半部分排序
sort(a, j + 1, high);// 右半部分排序
}
private int partition(Comparable[] a, int low, int high) {
Comparable index = a[low];// 切分标志
int i = low + 1, j = high;// i:左侧索引,j:右侧索引
while (true) {
while (less(a[++i], index)) {// a[i] < index时,i向中间移动,否则,i将被交换
if (i >= high) {// 边界
break;
}
}
while (less(index, a[--j])) {// index < a[j]时,j向中间移动,否则,j将被交换
if (j <= low) {// 边界
break;
}
}
if (i >= j) break;// 已经有序
exch(a, i, j);// 经过内循环后,有a[i] >= index >=a [j],交换i,j
}
exch(a, low, j);// 经过循环,j已经走到左侧部分的最右,此时交换low和j,保证切分标志在中间被排好。
return j;
}
}
###性能特点
快速排序的内循环只做比较,不移动数据,较之其他排序更快。
最好的情况是每次都能正好将数组对半分。对应的,最差的情况就是第一次从最小的元素切分,第二次从第二小的元素切分。。。每次调用只会确定一个元素。可以在排序前将数组随机,即shuffle。
###改进
- 小数组改用插入排序。对于小数组,插入排序要比快排更块,也减少了sort的递归调用。小数组就是 high - low 小于某个值的数组。通常5-15都会work well。
- 三中位切分。使用一部分的中位数作为切分元素,性能更佳。一般将取样大小设为3效果最好。
- Entropy-optimal sorting。含有大量重复元素的数组,有潜力从线性对数级提升到线性级。a。将数组切分成三部分,分别小于、等于、大于切分元素。即Dijkstra的荷兰旗问题,从左到右遍历数组,对于指针 lt,a[lo, …, lt-1]均小于切分元素 v,对于指针 gt ,a[gt+1,…,hi]均大于切分元素v,对于指针 i,a[lt, …, i-1]均等于v,a[i, …, gt]未确定。
初始 i 等于 lo,之后三向比较:
- 如果 a[i] 小于 v,交换 a[lt] 和 a[i], lt+1, i+1.
- 如果 a[i] 大于 v,交换 a[gt] 和 a[i], gt-1.
- 如果 a[i] 等于 v,i+1。
private static void sort(Comparable[] a, int lo, int hi) {
if (hi <= lo) return;
int lt = lo, gt = hi;
Comparable v = a[lo];
int i = lo + 1;
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);
}