快速排序
一.对比归并排序和快速排序
快速排序是一种分治的排序算法。它将一个数组分成两个子数组,将两部分独立地排序。快速排序和归并排序是互补的:归并排序将数组分成两个子数组分别排序,并将有序的子数组归并以将整个数组排序;而快速排序是当两个子数组都有序的情况时整个数组也就自然有序了。归并排序递归调用发生在处理整个数组之前,快速排序递归调用发生在处理整个数组之后。在归并排序中,一个数组被等分成两半;在快速排序中,切分的位置取决于数组的内容。
二.代码
public class Quick {
public static void sort(Comparable[] a){
sort(a, 0, a.length-1);
}
private static void sort(Comparable[] a,int lo,int hi){
if (hi <= lo + 10){
//对于小数组,我们转换为插入排序
Insertion.sort(a, lo, hi);
return;
}
int mid = partition(a, lo, hi);
sort(a, lo, mid-1);
sort(a, mid+1, hi);
}
}
partition()方法返回一个索引,该索引的元素的位置是被排定的,该索引左边的元素都小于等于这个索引的元素,该索引右边的元素都大于等于该索引的元素。然后向左边递归,再向右边递归。我们来看看partition()方法的代码:
private static int partition(Comparable[] a,int lo,int hi) {
int i = lo;
int j = hi+1;
Comparable temp = a[lo];
while (i<j){
while (less(a[++i],temp)){
if (i>hi){
break;
}
}
while (less(temp,a[--j])){
//下面这个判断是多余的
// if (j==lo){
// break;
// }
}
if (i>=j){
break;
}
exch(a, i, j);
}
exch(a, lo, j);
return j;
}
对于partition()方法,我们的策略是先随意地取a[lo]作为切分元素,即那个将会被排定的元素,然后我们从数组的左端开始向右扫描直到找到一个大于等于它的元素,再从数组的右端开始向左扫描直到找到一个小于等于它的元素。然后我们交换它们的位置。如此继续,我们可以保证左指针i的左侧元素都不大于切分元素,右指针j的右侧元素偶不小于切分元素。当两个指针相遇时,我们只需要将元素a[lo]和元素a[j]交换,然后返回j即可。
三.算法改进
1.切换到插入排序
和大多数递归排序算法一样,改进快速排序性能的一个简单办法基于以下两点
- 对于小数组,快速排序比插入排序慢
- 因为递归,快速排序的sort()方法在小数组也会调用自己
因此,在排序小数组时应该切换到插入排序。
if(hi <= lo) return;
替换成
if(hi <= lo + M){Insertion.sort(a,lo,hi);}
参数M的最佳值和系统有关,一般取5~15之间任意值
2.三向切分的快速排序
- 应用场景
实际应用中经常会出现含有大量重复元素的数组,例如我们可能需要将大量人员资料按照生日排序,或是按照性别区分开来。在这些情况下,我们实现快速排序的性能还有巨大的改进空间。比如,一个元素全部重复的子数组,就不需要继续排序了,但我们的算法还会继续将他切分为更小的数组。在有大量重复元素的情况下,快速排序的递归性会使元素全部重复的子数组经常出现,这就有很大的改进空间。 - 代码
public class Quick3way {
private static void sort(Comparable[] a,int lo,int hi){
if (hi<=lo) return;
int lt = lo;
int i = lo+1;
int gt = hi;
Comparable temp = a[lo];
while (i<=gt){
if (less(a[i],temp)){
exch(a, i, lt);
lt++;
i++;
}else if (a[i].compareTo(temp)==0){
i++;
}else {
exch(a, i, gt);
gt--;
}
}
sort(a, lo, lt-1);
sort(a, gt+1, hi);
}
/**
* 交换数组a的i位置和j位置的元素
* @param a
* @param i
* @param j
*/
private static void exch(Comparable[] a,int i,int j){
Comparable temp = a[i];
a[i] = a[j];
a[j] = temp;
}
/**
* 比较大小,如果v小于w就返回true
* @param v
* @param w
* @return
*/
private static boolean less(Comparable v,Comparable w){
return v.compareTo(w) < 0;
}
}
- 代码说明
三向切分的快速排序,它从左到右遍历数组一次,维护一个指针lt使得a[lo…lt-1]的元素都小于temp,一个指针gt使得a[gt…hi]的元素都大于temp,一个指针i使得a[lt…i-1]的元素都等于temp,a[i…gt]中的元素都还未确定。一开始i和lo相等,在遍历过程中我们会遇到下面三种情况:
- a[i]小于temp,将a[lt]和a[i]交换,将lt和i加一;
- a[i]大于temp,将a[gt]和a[i]交换,将gt减一;
- a[i]等于temp,将i++;
这段排序代码的切分能够将和切分元素相等的元素归位,这样它们就不会被包含在递归调用处理的子数组之中了。对于存在大量重复元素的数组,这种方法比标准的快速排序的效率高得多