快速排序
算法2.5 快速排序
import edu.princeton.cs.algs4.In;
import edu.princeton.cs.algs4.StdOut;
import edu.princeton.cs.algs4.StdRandom;
public class Quick {
public static void sort(Comparable[] a){
StdRandom.shuffle(a);
sort(a,0,a.length-1);
}
private static void sort(Comparable[] a, int lo, int hi){
//if(lo >= hi + M) {Insertion(a,lo,hi); return; }//改进,小数组采用插入排序
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){
int i = lo;
int j = hi + 1;
Comparable v = a[lo];//默认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);//只要i,j未相遇,则交换两处元素
}
exch(a,lo,j);//最终把切分元素与左子数组最后元素交换位置
return j;
}
//...
}
要点
- 快速排序是一种分治的排序算法,和归并互补:归并把数组分成两个子数组分别排序,然后归并两个有序的子数组使整体有序;快排是当两个子数组都有序时整个数组自然也有序了.归并时,递归sort发生在归并merge之前,快排中,归并sort发生在分切partition后
- 切分的一般策略:随意取a[lo]作为切分元素,然后从左到右直到找到>=它的元素,再从右到左找到<=它的元素,然后在两个指针未相遇时就可以交换两元素,指针相遇时,只需把a[lo]和左子数组最右侧元素交换,最终切分元素左侧的元素都比它小,右侧元素都比它大,再各自排序.
- sort中有StdRandom.shuffle方法,为了保证元素的随机性,避免切分时出现极大子数组和极小子数组的不均衡情况.
- 对于有大量重复值的情况,一般的快排不可避免会发生等值元素继续交换位置的现象.
特点
- 快排切分的内循环会用一个递增的索引(i++,j++)将数组元素和一个定值(v)比较,没有移动数据,而归并和希尔一般比快排慢,在于它们在内循环中移动数据
算法改进
- 小数组使用插入排序
if(lo >= hi + M) {Insertion(a,lo,hi); return; }//改进,小数组采用插入排序
- 取消while的边界测试—三取样切分:见Ex2_3_18
- 应对大量重复元素—三向切分:如下
三向切分
public class Quick3Way {
public static void sort(Comparable[] a){
StdRandom.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, i = lo+1, 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++;
}
sort(a,lo,lt-1);//[lo,lt-1]间元素都小于v
sort(a,gt+1,hi);//[gt+1,hi]间元素都大于v
}
//...
}
分析
三项切分快速排序:从左到右遍历数组,指针lt使得a[lo…lt-1]中元素
- a[i] < v,交换a[lt]和a[i],lt++,i++;
- a[i] > v,交换a[gt]和a[i],gt—;
- a[i] == v, i++;
Review
- 一般快速排序:
- partition()中i从lo开始,j从hi+1开始,巧妙配合while中的++i和—j,保证从lo+1处开始扫描完全
- exch(a,lo,j)最后是j不是i,因为++i和—j的缘故,循环最后一步,i指向右子数组左侧,j指向左子数组右侧
- int j = partition(a,lo,hi);j即是分切元素,已经确定好了位置,无需排序,所以sort(a,lo,j-1);sort(a,j+1,hi);
- 三项切分快排:
- 无partition(),切分直接在sort()中完成;
- 三个变量,lt是”三项”的中间项起点,gt为中间项终点;
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++;
}
- i与否问题:
while中,因为每次i都会变化,cmp需要每次重新计算.
(cmp<0)时,lt且i++,若此处i不变,第一次交换后a[i]=a[lo],cmp=0,依旧i++,所以exch()中直接执行;(cmp>0)时,gt—,i不变,因为第一次交换后,需再次比较换下来的a[gt]否小于v; - while()循环条件之所以为(i<=gt):
原因与第三点最后相同:
若换下一个a[gt]后gt—,i == gt,即此时到了中项和右项交界,必须再判断一次换下的元素是否还小于等于v,甚至大于v,当小于等于v后i++,i > gt,跳出循环,当大于v后gt—, gt < i,跳出循环.