此博客用于个人学习,来源于算法的书籍和网上的资料,对知识点进行一个整理。
1. 概述:
快速排序是一种分治的排序算法,它将一个数组分成两个子数组,将两部分独立地排序。快速排序和归并排序是互补的:归并排序是将数组分成两个子数组分别排序,并将有序的子数组归并以将整个数组排序;而快速排序将数组排序的方式则是当两个子数组都有序时整个数组也就自然有序了。在第一种情况中,递归调用发生在处理整个数组之前;在第二种情况中,递归调用发生在处理整个数组之后。在快速排序中,切分的位置取决于数组的内容。
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 即可。
3. 代码实现:
/**
* 快速排序
*/
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...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;
}
}
当数组中的重复元素的数量很多的时候,可以对快速排序进行算法改进,讲数组分成三部分,分别对应小于,等于和大于切分元素的数组元素。
/**
* 快速排列的改进版本
*/
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;
}
}
4. 特点:
- 优点:
- 原地排序,只需要一个很小的辅助栈。且将长度为 N 的数组排序所需的时间和 NlgN 成正比。
- 内循环比大多数排序算法都要短小,这意味着它无论是理论上还是实际上要更快。
- 缺点:非常脆弱,在实现的时候要非常小心才能避免低劣的性能。
5. 算法分析:
-
时间复杂度:与归并排序的分析一致,为 O(nlogn)。
-
空间复杂度:主要是递归造成的栈空间的使用,类似于树的结构,递归树的深度为 logn,其空间复杂度也就为 O(logn)。
-
稳定性:不稳定的,不稳定是由于我们在插入元素的时候使用了互换。而不是直接把插入位置后面所有元素整体往后移动一位,再填进要插入的数。例如:
5 | 3 1 2 | 9 7 8 9 | 4 6 3
这时遍历 unvisited 部分,刚到了4 (array[8]) 显然4<5 ,这是4应该从 unvisited 部分去到 lower 部分。 因此 higher 部分第一个元素 9 (array[4]) 和 4互换。变成了这样:
5 | 3 1 2 4 | 7 8 9 9 | 6 3
这时这个9 (array[4]) 被换到了 后面那个9 (array[7])的 后面。这就不稳定了。