此博客用于个人学习,来源于算法的书籍,对知识点进行一个整理。
快速排序引人注目的特点包括它是原地排序(只需要一个很小的辅助栈),且将长度为 N 的数组排序所需要的时间与 NlgN 成正比。
1. 基本算法:
快速排序是一种分治的排序算法,它将一个数组分成两个子数组,将两部分独立地排序。快速排序和归并排序是互补的:归并排序是将数组分成两个子数组分别排序,并将有序的子数组归并以将整个数组排序;而快速排序将数组排序的方式则是当两个子数组都有序时整个数组也就自然有序了。在第一种情况中,递归调用发生在处理整个数组之前;在第二种情况中,递归调用发生在处理整个数组之后。在快速排序中,切分的位置取决于数组的内容。
/**
* 快速排序
*/
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){
return;
}
int j = partition(a,lo,hi);
//将左半部分a[lo...j-1]排序
sort(a,lo,j-1);
//将右半部分a[j...hi]排序
sort(a,j,hi);
}
}
快速排序递归地将子数组 a[lo…hi] 排序,先用 partition() 方法将 a[j] 放到一个合适位置,然后再用递归调用将其他位置的元素排序。
2. 切分方法实现:
该方法的关键在于切分,这个过程使得数组满足下面三个条件:
- 对于某个 j,a[j] 已经排定;
- a[lo] 到 a[j-1] 中的所有元素都不大于 a[j];
- a[j+1] 到 a[hi] 中的所有元素都不小于 a[j]。
我们就是通过递归调切分来排序,因为切分过程总是能排定一个元素,用归纳法不难证明递归能够正确地将数组排序:如果左子数组和右子数组都是有序的,那么左子数组(有序且没有任何元素大于切分元素),切分元素和右子数组(有序且没有任何元素小于切分元素)组成的结果数组也一定是有序的。
切分方法的实现策略——先随意地取 a[lo] 作为切分元素,即那个将会被排定的元素,然后我们从数组的左端开始向右扫描直到找到一个大于等于它的元素,再从数组的右端开始向左扫描直到找到一个小于等于它的元素。这两个元素显然是没有排定的,因此我们可以交换它们的位置。如此继续,这样就可以保证左指针 i 的左侧元素都不大于切分元素,右指针 j 的右侧元素都不小于切分元素。当两个指针相遇时,我们只需要将切分元素 a[lo] 和左子数组最右侧的元素( a[j] )交换然后返回 j 即可。
//快速排列的划分,将数组划分为a[lo...i-1],a[i],a[i+1...hi]
private static 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;
exchange(a,i,j);
}
exchange(a,lo,j);
return j;
}
private static void exchange(Comparable[] a,int i,int j){
Comparable t = a[i];
a[i] = a[j];
a[j] = t;
}
private static boolean less(Comparable v,Comparable w){
return v.compareTo(w) < 0;
}
这段代码按照 a[lo] 的值 v 进行划分,当指针 i 和 j 相遇时主循环退出。在循环中,a[i] 小于 v 时,我们增大 i,a[j] 大于 v 时我们减小 j,然后交换 a[i] 和 a[j] 来保证 i 左侧的元素都不大于 v,j 右侧的元素都不小于 v。当指针相遇交换 a[lo] 和 a[j],切分结束(这样切分值就留在 a[j] 中了)。
3. 算法改进——三向切分:
实际应用中经常出现含有大量重复元素的数组,例如我们可能将大量人员资源按照生日排序,或者是按照性别区分开。这个时候,就需要改进我们的快速排序算法了,因为我们的算法遇到一个元素全都是重复元素的子数组时,还会继续将它切分为更小的数组。
将数组切分为三部分,分别对应小于,等于和大于切分元素的数组元素。
public class Quick3way {
private static void sort(Comparable[] a,int lo,int hi){
if (hi <= lo){
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){
exchange(a,lt++,i++);
}else if (cmp > 0){
exchange(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);
}
private static void exchange(Comparable[] a,int i,int j){
Comparable t = a[i];
a[i] = a[j];
a[j] = t;
}
}
这段排序代码的切分能够将和切分元素相等的元素归位,这样它们就不会被包含在递归调用处理的子数组之中了。对于存在大量重复元素的数组,这种方法比标准的快速排序的效率高多了。