快速排序
快速排序是应用最广泛的排序算法了。原因是实现简单,且在一般应用中比其他排序算法要快得多。
快速排序内循环比大多数排序算法都要短小,这意味着它无论是在理论上还是在实际中都要更快。但是它的缺点是非常脆弱,在实现中要非常小心才能避免低劣的性能。
在归并排序这篇文章中有讲解到“分治策略”,该策略一般是将一个大问题,(递归的)分为 n 个小问题处理解决。
快速排序也是一种分治的排序算法,它将一个数组分成 2 个子数组,将两部分独立地排序。
快速排序和归并排序是互补的 :
归并排序 : 将数组平分成 n 个子数组分别排序,并将排序后的子数组层层归并,从而将整个数组排序。(由小到大)
快速排序 : 处理原则是当两个子数组都有序时整个数组也自然有序了。
基本思想
快速排序的基本思想是 :
通过一趟排序将要排序的数组分成 2 个独立的数组,其中左半部分的数组都比右半部分的数组小。再依此递归对这2个数组再排序。
通俗来说,快速排序是基于“轴”的排序算法,在数组中挑选一个元素作为轴,比轴小的元素在轴的左边,比轴大的元素放置轴的右边。
上图阐述的是快速排序的算法思想。
在代码中,首先将数组 a [ lo … hi ] 排序,先用 partition()
方法将 a [ j ] 放到一个合适位置,(将数组切分成 2 个子数组)然后在用递归调用将其他位置的元素排序。
切分
private static int partition(int a[], int lo, int hi) {//该算法过程仍可优化
int i = lo, j = hi + 1;//i 和 j两个指针
int v = a[lo];//轴
while (true) {
while (a[++i] < v) {//从左至右,找到大于轴的元素
if (i >= hi) {break;}
}
while (a[--j] > v) {//从右至左,找到小于轴的元素
if (j <= lo) {break;}
}
if (i >= j) {break;}
exch(a, i, j);
}
//轴原本在lo下标,移至j下标
//此时a[lo .. j-1]的元素都小于轴 ,a[j+1 .. hi]的元素都大于轴
exch(a, lo, j);
return j;
}
这段代码按照 a[lo] 的值 v 作为轴进行切分。当指针 i 和 j 相遇时主循环退出。在循环中, a[ i ] 小于 v 时增大 i ( i++ ),a[ j ] 大于 v 时我们减小 j 。然后交换 a[ i ] 和 a[ j ] 来保证 i 的左侧元素都小于 v ,j 的右侧元素都大于 v 。当指针相遇时交换 a[lo] 和 a[ j ],切分结束。(这样切分值就留在a[ j ]中了)
对于切分,这个过程会使得数组满足下面三个条件 :
- 对于某个 j ,a[ j ] 已经排定;
- a[ lo ] 到 a[ j-1 ] 中的所有元素都小于 a[ j ];
- a[ j+1 ] 到 a[ hi ] 中的所有元素都大于 a[ j ];
选取【算法4】图解增加理解
排序算法
捋清了快速排序的切分过程之后,整体的算法过程便清晰起来 :
public static void main(String[] args) {
int[] a = new int[] { 9, 1, 2, 8, 7, 3, 5, 4 };
sort(a, 0, a.length - 1);
}
private static void sort(int a[], int lo, int hi) {
if (lo >= hi) {
return;
}
int j = partition(a, lo, hi);//切分
sort(a, lo, j - 1);//轴左边数组排序
sort(a, j + 1, hi);//轴右边数组排序
}
算法改进
快速排序是由C.A.R Hoare在1960年发明提出的,至今仍有很多改进的方法。
-
小数组切换到插入排序
和大多数递归排序算法一样,因为递归,快速排序的sort()
方法在小数组中也会调用自己。所以在小数组中,快速排序比插入排序慢。 -
数组中存在重复元素
如果要排序的子数组中元素都是重复的那就不需要继续排序了,但上面的算法还会继续将它切分为更小的数组。针对这一问题,我们使用三向切分的快速排序。
三向切分(改进重复元素排序)
三向切分主要是解决上图问题,即减少重复数组中的递归次数。
private static void sort(int a[], int lo, int hi) {
if (lo >= hi) {
return;
}
int j = partition(a, lo, hi);//切分
sort(a, lo, j - 1);//轴左边数组排序
sort(a, j + 1, hi);//轴右边数组排序
}
在代码中我们看到,快速排序会将 a [ lo … j ] ,a[ j … hi ] 的数组继续递归排序,此时我们可以引入一个指针 mid (可自行命名)用来指向重复区域,使得 a [ i … j ] 区域的元素都为重复元素,此时我们只要将 a [ lo … i] , a [ j … hi ] 排序即可。
我们新引入指针 mid :
- a[ mid ] 等于 v,mid +1;
- a[ mid ] 小于 v,交换 a[ i ] 和 a[ mid ]的值,并将 i 和 mid + 1;
- a[ mid ] 大于 v,交换 a[ j ] 和 a[ mid ]的值,并将 j -1;
- 当 mid 和 j 指针相遇时退出循环,a [ i … mid ] 区间的值都为和v相同的元素集。
上图可以说是比较清晰的表达了三向切分快速排序的这一过程,它和我们第一个介绍的快速排序实现有所不同,但是基本思想都是将数组分成 2 部分,左部分的元素均小于 轴,右部分的元素均大于 轴。
//代码也略有不同,不过三向切分的代码相对来说更容易理解些。
public static void main(String[] args) {
int[] a = new int[] { 9, 1, 2, 8, 7, 3, 5, 4 };
sort(a, 0, a.length - 1);
}
private static void sort(int a[], int lo, int hi) {
if (lo >= hi) {
return;
}
int i = lo, mid = lo + 1, j = hi;
int v = a[lo];
while (mid <= j) {
int cmp = a[mid] - v;
if (cmp < 0) {
exch(a, mid++, i++);
} else if (cmp > 0) {
exch(a, mid, j--);
} else {
mid++;
}
}
sort(a, lo, i - 1);
sort(a, j + 1, hi);
}
该算法遵守上面我们提到的 :
- a[ mid ] 等于 v,mid +1;
- a[ mid ] 小于 v,交换 a[ i ] 和 a[ mid ]的值,并将 i 和 mid + 1;
- a[ mid ] 大于 v,交换 a[ j ] 和 a[ mid ]的值,并将 j -1;
- 当 mid 和 j 指针相遇时退出循环,a [ i … mid ] 区间的值都为和v相同的元素集。