详彻快速排序原理分析
归并排序不足之处
约翰.冯.诺伊曼
(John von Neumann)在1945年提出归并排序,其时间复杂度为(nlogn)
,但归并排序不是原地排序算法,空间复杂度比较高,是O(n)。所以托尼.霍尔
(Tony Hoare)在1961年提出快速排序,最差:O(n²),期望O(nlogn).另外,快速排序的内循环比大多数排序算法都要短小,这意味着它无论是在理论上还是在实际中都要快。它的主要缺点是非常脆弱,在实现时要非常小心才能避免低劣的性能。幸好有相应的解决方式,通过随机性算法来打乱数组,以避免最差的情况发生。
归并排序与快速排序算法思路比较
快排亦是基于一种分治的排序算法。快速排序和归并排序是互补的:归并排序将数组分成两个子数组分别排序,并将有序的子数组归并以将整个数组排序;而快速排序将数组排序的方式则是当两个子数组都有序时整个数组也就自然有序了。在第一种情况中,递归调用发生在处理整个数组之前;在第二种情况中,递归调用发生在处理整个数组之后。
数组划分原理
- 选取固定位置主元x(如尾元素)
- 维护两个部分的右端点变量i,j
- 考察数组元素A[j],
只和主元比较
- 把主元放在中间作分界线
若A[j]<=x,则交换A[j]和A[i+1],i,j右移
若A[j]>x,则j右移
过程态
初始态
结果态
把主元放在中间作分界线
数组划分:伪代码
/**
*Partition(A,p,r)
*输入:数组A,起始位置p,终止位置r
*输出:划分位置q
*/
x <-- A[r] //选取主元
i <-- p-1
for j <-- p to r - 1 do //时间复杂度为O(n)
if A[j] <=x then
exchange A[i + 1] with A[j]
i <-- i + 1
end
end
exchange A[i + 1] with A[r] //主元作分界线
q <-- i + 1
return q
快速排序:伪代码
/**
*QuickSort(A,p,r) 初始调用: QuickSort(A,1,n)
*输入:数组A,起始位置p,终止位置r
*输出: 有序数组A
*/
if p < r then
q <-- Partition(A,p,r) 最好情况O(n) 最坏情况O(n)
QuickSort(A,p,q-1) T(n / 2) T(0)
QuickSort(A,q+1,r) T(n / 2) T(n-1)
end
T(n) = 2T(n/2) + O(n) = O(nlogn)
T(n) = T(n-1) + T(0) + O(n) = O(n²)
随机划分考虑
反思最差情况
数组划分时选取固定位置主元,可以针对性构造最差情况
解决方案
数组划分时选取随机位置主元,无法针对性构造最差情况
Randomized-Partition(A,p,r)
//输入:数组A,起始位置p,终止位置r
//输出:划分位置q
s <-- Random(p,r) //随机选取主元
exchange A[s] with A[r]
q <-- Partition(A,p,r)
return q
Randomized-QuickSort<A,p,r>
//输入:数组A,起始位置p,终止位置r
//输出:有序数组A
if p<r then
q <-- Randomized-Partition(A,p,r)
Randomized-QuickSort(A,p,q - 1)
Randomized-QuickSort(A, q+1 , r)
end
随机化快速排序:复杂度分析
所以时间复杂度为O(nlogn)
另一种常见的快速排序代码实现方式
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(hi <= lo) 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){
//将数组切分为a[lo..i-1],a[i], a[i+1..hi]
//抛开主元,lo与hi 都指向边界。
int i = lo, j = hi + 1;
Comparable v = a[lo];
//假设主元是4,其中一部分顺序是3 5 7,最后j会指向3,i会指向5
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);
}
//将v = a[j]放入正确的位置
exch(a, lo, j);
//a[lo..j-1] <= a[j] <= a[j+1..hi]达成
return j;
}
private static boolean less(Comparable v, Comparable w){
return v.compareTo(w) < 0;
}
private static void exch(Comparable[] a, int i, int j){
Comparable t = a[i];
a[i] = a[j];
a[j] = t;
}
public static void main(String[] args) {
Character[] a = {'K','R','A','T','E','L','E','P','U','I','M','Q','C','X','O','S'};
sort(a);
for(char i : a){
System.out.println(i);
}
}
}
三向切分的快速排序
实际应用中经常会出现含有大量重复元素
的数组,例如我们可能需要将大量人员资料按照生日排序,或是按照性别区分开来。其中全部重复的子数组就不需要继续序
,但我们的算法还会继续将它切分为更小的数组,快速排序的递归性会使元素全部重复的子数组经常出现,这就有很大的改进潜力,将当前实现的线性对数级的性能提高到线性级别
。一个简单的想法是将数组切分为三部分,分别对应小于、等于和大于切分元素的数组元素
。
具体算法思想如下:它从左到右遍历数组一次,维护一个指针lt,使得a[lo..lt-1]中的元素都小于v
,一个指针gt,使得a[gt+1..hi]中的元素都大于v
, 一个指针i使得a[lt..i-1]中的元素都等于v
,a[i…gt]中的元素都还未确定。我们使用Comparable接口(而非less())对a[i]进行三向比较来直接处理以下情况:
- a[i]小于v,将a[lt]和a[i]交换,将lt和i加一;
- a[i]大于v,将a[gt]和a[i]交换,将gt减一;
- a[i]等于v,将i加一;
这些操作都会保证数组元素不变且缩小gt-1的值
(这样循环才会结束)。另外,除非和切分元素相等,其它元素都会被交换。
import edu.princeton.cs.algs4.StdRandom;
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) exch(a, lt++, i++);
else if(cmp > 0)exch(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);
}
public static void sort(Comparable[] a) {
StdRandom.shuffle(a);
sort(a, 0, a.length - 1);
}
private static void exch(Comparable[] a, int i, int j){
Comparable t = a[i];
a[i] = a[j];
a[j] = t;
}
public static void main(String[] args) {
Character[] a = {'R','B','W','W','R','W','B','R','R','W','B','R'};
sort(a);
for(char i : a){
System.out.print(i + "\t");
}
}
}