《算法(第四版)》2.3.17:哨兵,2.3.18:三取样切分
哨兵。正如书中所说,while循环中的边界检查是多余的,对于左侧边界,v不可能小于a[lo],故左侧边界的检查是多余的;对于右侧边界,只要将数组最大元素放置到末尾该元素就永远不会移动,v不可能大于a[hi]。
标准快排和哨兵快排关键代码对比:
标准快排
public void sort (Comparable[] a) {
StdRandom.shuffle(a);
sort(a, 0, a.length - 1);
}
哨兵快排,先将最大元素置于末尾
public void sort (Comparable[] a) {
StdRandom.shuffle(a);
exch(a, maxIndex(a), a.length - 1);
sort(a, 0, a.length - 1);
}
标准快排,有边界检查
private int partition(Comparable[] a, int lo, int 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);
return j;
}
哨兵快排,无边界检查
private int partition(Comparable[] a, int lo, int hi) { //划分
int i = lo, j = hi +1;
Comparable v = a[lo];
while(true) { //扫描左右,检查是否结束并交换元素
while(less(v, a[--j])); //去掉左边边界检查
while(less(a[++i], v)); //去掉右边边界检查
if(i >= j) break;
exch(a, i, j);
}
exch(a, lo, j);
return j;
}
对标准快排和哨兵法快排进行双倍测试,每次测试数组长度为2000,测试次数为4000。在某次测试中,快排历时约0.683秒,哨兵快排历时约0.508秒。可见磨刀不误砍柴工,在排序前遍历数组得到最大元素是有必要的,边界检查付出的时间代价可能更大。
三取样切分。按照书中的描述,改进快排的一个方法是使用数组的一小部分元素的中位数来切分数组,但是代价是计算中位值,当取样大小为3时效果最好。同样,将取样元素放在数组末尾作为哨兵来去掉数组边界检测。具体措施是,先对比取样的3个元素是否有大于数组末尾的元素,若有则与其置换;然后将取样位置上的3个元素中的中位数作为切分元素置于最前。
三个元素确定中位数的方法为做差相乘,中位数在第二位:(a[lo+1] - a[lo]) * (a[lo+1] - a[lo+2]) <= 0;中位数在第三位:(a[lo+2] - a[lo]) * (a[lo+2] - a[lo+1]) <= 0;剩下一种可能就是中位数在首位。
关键代码:
private void mid(Comparable[] a, int lo, int hi) {
for(int i = 0; i < 3; i ++) { //3个元素中的最大值若大于hi项,则置换
if(less(a[hi], a[lo+i])) exch(a, hi, lo+i);
}
if(a[lo+1].compareTo(a[lo]) * a[lo+1].compareTo(a[lo+2]) <= 0) exch(a, lo, lo+1);
else if(a[lo+2].compareTo(a[lo]) * a[lo+2].compareTo(a[lo]) <= 0) exch(a, lo, lo+2);
}
切分
private int partition(Comparable[] a, int lo, int hi) {
int i = lo, j = hi + 1;
if(hi - lo >= 2) {
mid(a, lo, hi);
Comparable v = a [lo];
while(true) {
while(less(v, a[--j]));
while(less(a[++i], v));
if(i >= j) break;
exch(a, i, j);
}
exch(a, lo, j);
} else if(less(a[hi], a[lo])) {
exch(a, hi, lo); j --;
} else j --;
return j;
}
同样地进行双倍测试,3取样快排历时0.560秒,对三个元素求中位数的时间代价可能小于边界比较的时间代价,但总的代价也可能大于遍历求最大元素的代价。
若要显著提升快速排序的速度,需要结合插入排序,当子数组元素个数较小时,次数子数组呈上升趋势,此时采用插入排序可大大提升排序速度。