尽管有前面复杂度较好的归并排序,但当今大多数排序程序都是基于快速排序算法。它是英国计算机科学家C.A.R.(Tony) Hoare发明的。
wikipeda上的gif图描述快排:
和前面归并排序一样,快速排序也采用了分而治之的策略,不过它在拆的时候就让序列“稍微有序”,越拆越有序,最后就不用像归并一样重建数组了。
如何做到稍微有序呢?就是让偏大的元素尽量靠近序列末尾,较小的元素出现在序列开头。
例如,对于下列一个序列:
那么我们如何定义大和小呢?对于上面的序列,如果我们以50为分界线,比50大或等于的定义为‘大’,比50小的定义为‘小’,那么序列就会变成这个样子:
如果将这个过程递归下去,那么就能得到一个有序序列了,这就是快排的基本思想。
与合并排序一样,快排的简单情况是大小为0或1的数组,因为它们已经是排好序的了。快速排序算法中的递归部分包含以下步骤:
1、选择一个元素作为小元素和大元素的之间的边界,如上面的50。这个元素被称为支点。目前,挑选任何元素都可以满足此目的,最简单的策略就是选择数组的第一个元素。
2、重新排列数组中的元素,使大元素移向数组的末尾,小元素移向数组的开头。简单来讲,这一步的目标是将元素围绕着边界位置进行分割,使得该边界左边的所有值都小于支点,而右边的所有值都大于或等于支点。这个过程被称为划分数组。
3、对每个划分的数组进行排序。局部有序保证整体有序,整体有序是局部有序的前提。先整体,后局部,分而治之。
那么如何划分数组呢?我们需要两个“指针”或者叫“索引手”来进行。
还是上面的序列,我们选第一个元素为支点。
1、用两个指针,左手lh指向剩余元素的第一个元素,右手rh指向最后一个元素。就像这样:
2、将rh索引向左移动,直至它与lh重合或者指向的元素小于支点。在这里,位置7已经是小值了,因此rh不需要移动。
3、将lh索引向右移动,直至它与rh重合或者指向的元素值大于或等于支点。在这里,lh会移动到元素值为58的三号位置。
4、如果lh和rh索引值还没有到达相同的位置,那么就交换lh和rh位置的元素,像这样:
5、重复步骤2~4,直至lh和rh位置重合。例如,下一趟中,19和95将会交换。最终rh会与lh撞到一起:
6、除非所选的支点碰巧是整个数组中最小的元素(并且代码中包含了特殊的对这种情况的检查),否则lh和rh索引重合的位置就是数组中最右侧的小值所在的位置。剩下的步骤就是将这个值与位于数组开头处的支点元素互换:
示例代码:
public class QuickSort {
public void sort(int[] array) {
quicksort(array, 0, array.length);
}
private void quicksort(int[] array, int p1, int p2) {
if (p2 - 1 <= p1) return;
int boundary = partition(array, p1, p2);
quicksort(array, p1, boundary);
quicksort(array, boundary + 1, p2);
}
private int partition(int[] array, int p1, int p2) {
int pivot = array[p1];
int lh = p1 + 1;
int rh = p2 - 1;
while (true) {
while (lh < rh && array[rh] >= pivot) rh--;
while (lh < rh && array[lh] < pivot) lh++;
if (lh == rh) break;
int tmp = array[lh];
array[lh] = array[rh];
array[rh] = tmp;
}
if (array[lh] >= pivot) return p1;
array[p1] = array[lh];
array[lh] = pivot;
return lh;
}
public static void main(String[] args) {
int[] array = new int[]{56, 25, 37, 58, 95, 19, 73, 30};
new QuickSort().sort(array);
for (int n : array)
System.out.printf("%-5d", n);
}
}
复杂度分析:
平均情况O(N log N)
最差情况O(N^2)