标题
DualPivotQuicksort
1. Arrays 中的应用
- 在之前的博文中,提到过数组中7种排序方法(冒泡排序、插入排序、选择排序、希尔排序、快速排序、合并排序、桶排序),详见:Java 的数组排序;
- 那么 Arrays 类中用的是哪种方法呢?我们看下对应的源代码:
/*
* Sorting methods. Note that all public "sort" methods take the
* same form: Performing argument checks if necessary, and then
* expanding arguments into those required for the internal
* implementation methods residing in other package-private
* classes (except for legacyMergeSort, included in this class).
*/
/**
* Sorts the specified array into ascending numerical order.
*
* <p>Implementation note: The sorting algorithm is a Dual-Pivot Quicksort
* by Vladimir Yaroslavskiy, Jon Bentley, and Joshua Bloch. This algorithm
* offers O(n log(n)) performance on many data sets that cause other
* quicksorts to degrade to quadratic performance, and is typically
* faster than traditional (one-pivot) Quicksort implementations.
*
* @param a the array to be sorted
*/
public static void sort(int[] a) {
DualPivotQuicksort.sort(a, 0, a.length - 1, null, 0, 0);
}
/**
* Sorts the specified range of the array into ascending order. The range
* to be sorted extends from the index {@code fromIndex}, inclusive, to
* the index {@code toIndex}, exclusive. If {@code fromIndex == toIndex},
* the range to be sorted is empty.
*
* <p>Implementation note: The sorting algorithm is a Dual-Pivot Quicksort
* by Vladimir Yaroslavskiy, Jon Bentley, and Joshua Bloch. This algorithm
* offers O(n log(n)) performance on many data sets that cause other
* quicksorts to degrade to quadratic performance, and is typically
* faster than traditional (one-pivot) Quicksort implementations.
*
* @param a the array to be sorted
* @param fromIndex the index of the first element, inclusive, to be sorted
* @param toIndex the index of the last element, exclusive, to be sorted
*
* @throws IllegalArgumentException if {@code fromIndex > toIndex}
* @throws ArrayIndexOutOfBoundsException
* if {@code fromIndex < 0} or {@code toIndex > a.length}
*/
public static void sort(int[] a, int fromIndex, int toIndex) {
rangeCheck(a.length, fromIndex, toIndex);
DualPivotQuicksort.sort(a, fromIndex, toIndex - 1, null, 0, 0);
}
- 可见,用的是 DualPivotQuicksort,那么什么是 DualPivotQuicksort 呢?
2. QuickSort
- Quick Sort(快速排序)在1960年由 C.A.R. Hoare 提出,于1990年代稳定下来,成为几乎所有的语言里面的排序方法;
- 基本思想是选择一个数做为 pivot(基准),将数组分为两部分,比基准数小的放在左边,比基准数大的放在的右边,然后分别对左边一部分和右边一部分重复上述操作,整个排序过程可以递归进行,直到子数组只剩一个数为止,整个数据变成有序序列;
- Java 实例如下:
public class Test {
/*
思路:
p <- Get a number from array
Loop if left < right
Put elements <= p to the left side
Put elements >= p to the right side
Recursive quickSort the left parts and right parts
*/
public static void quickSort(int[] arr, int head, int tail) {
int start = head;
int end = tail;
int pivot = arr[head];
while (start < end) {
//从后往前开始比较
while (end > start && arr[end] >= pivot)//如果没有比关键值小的,比较下一个,直到有比关键值小的交换位置,然后又从前往后比较
end--;
if (arr[end] <= pivot) {
int temp = arr[end];
arr[end] = arr[start];
arr[start] = temp;
}
//从前往后比较
while (end > start && arr[start] <= pivot)//如果没有比关键值大的,比较下一个,直到有比关键值大的交换位置,然后又从后往前比较
start++;
if (arr[start] >= pivot) {
int temp = arr[start];
arr[start] = arr[end];
arr[end] = temp;
}
//此时第一次循环比较结束,关键值的位置已经确定了。左边的值都比关键值小,右边的值都比关键值大,但是两边的顺序还有可能是不一样的,进行下面的递归调用
}
//递归
if (start > head)
quickSort(arr, head, start - 1);
if (start < tail)
quickSort(arr, end + 1, tail);
}
public static void main(String[] args) {
int array[] = {1, 66, 88, 22, 3};
quickSort(array, 0, array.length - 1);
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + " ");
}
}
}
- 实际效果图如下:
- 快速排序的一次划分算法从两头交替搜索,直到 low 和 high 重合,因此其时间复杂度是 O(n);而整个快速排序算法的时间复杂度与划分的趟数有关;
- 理想的情况是,每次划分所选择的中间数恰好将当前序列几乎等分,经过 log2n 趟划分,便可得到长度为1的子表。这样,整个算法的时间复杂度为 O(nlog2n);
- 最坏的情况是,每次所选的中间数是当前序列中的最大或最小元素,这使得每次划分所得的子表中一个为空表,另一子表的长度为原表的长度-1。这样,长度为n的数据表的快速排序需要经过 n 趟划分,使得整个排序算法的时间复杂度为 O(n2);
- 为改善最坏情况下的时间性能,可采用其他方法选取中间数。通常采用“三者值取中”方法,即比较 H->r[low].key、H->r[high].key与H->r[(10w+high)/2].key,取三者中关键字为中值的元素为中间数;
- 可以证明,快速排序的平均 时间复杂度 也是 O(nlog2n)。因此,该排序方法被认为是目前最好的一种内部排序方法;
- 从空间性能上看,尽管快速排序只需要一个元素的辅助空间,但快速排序需要一个栈空间来实现递归。最好的情况下,即快速排序的每一趟排序都将元素序列均匀地分割成长度相近的两个子表,所需栈的最大深度为 log2(n+1);但最坏的情况下,栈的最大深度为 n。这样,快速排序的 空间复杂度 为 O(log2n));
3. DualPivotQuicksort
- DualPivotQuicksort(双轴快速排序)在2009年由 Yaroslav Vladimirskiy 提出;
a. Dual-pivot Quicksort Algorithm
- 研究论文如下:
b. DualPivotQuicksort 源码分析
- 快速排序分割图示:
left part center part right part
+--------------------------------------------------------------+
| < pivot1 | pivot1 <= && <= pivot2 | > pivot2 |
+--------------------------------------------------------------+
^ ^
| |
less great
- 双轴快速排序与快速排序的主要的区别就是经典快速排序递归的时候把输入数组分两段,而Dual-Pivot则分三段,分割图示:
Partitioning:
left part center part right part
+----------------------------------------------------------+
| == pivot1 | pivot1 < && < pivot2 | ? | == pivot2 |
+----------------------------------------------------------+
^ ^ ^
| | |
less k great
Invariants:
all in (*, less) == pivot1
pivot1 < all in [less, k) < pivot2
all in (great, *) == pivot2
Pointer k is the first index of ?-part.
- Java 实例:
import java.util.Arrays;
public class Test {
public void dualQuickSort(int[] A, int left, int right) {
if (left >= right) {
return;
}
if (A[left] > A[right]) {
swap(A, left, right);
}
int less = left;
int great = right;
int pivot1 = A[left];
int pivot2 = A[right];
while (A[++less] < pivot1) ;
while (A[--great] > pivot2) ;
/*
* Partitioning:
*
* left part center part right part
* +--------------------------------------------------------------+
* | < pivot1 | pivot1 <= && <= pivot2 | ? | > pivot2 |
* +--------------------------------------------------------------+
* ^ ^ ^
* | | |
* less k great
*
* Invariants:
*
* all in (left, less) < pivot1
* pivot1 <= all in [less, k) <= pivot2
* all in (great, right) > pivot2
*
* Pointer k is the first index of ?-part.
*/
outer:
for (int k = less - 1; ++k <= great; ) {
int ak = A[k];
if (ak < pivot1) { // ak 小于 p1
swap(A, k, less); // 交换
less++;
} else if (ak > pivot2) { // ak > p2
while (A[great] > pivot2) { // 找到不满足条件的位置
if (great-- == k) {
System.out.println("outer");
break outer;
}
}
if (A[great] < pivot1) { // a[great] <= pivot1,
A[k] = A[less]; // less放到 k的位置, k 位置的元素数保存在 ak中
A[less] = A[great]; // great 放到less的位置
++less; // 更新 less
} else { // pivot1 <= a[great] <= pivot2
A[k] = A[great];
}
/*
* Here and below we use "a[i] = b; i--;" instead
* of "a[i--] = b;" due to performance issue.
*/
A[great] = ak; // ak 放到 great位置
--great;
} // 其他情况就是中间位置,不用考虑
}
System.out.println("left :" + left + " less " + less + " great" + great + " right " + right);
System.out.println(Arrays.toString(A));
dualQuickSort(A, left, less - 1);
dualQuickSort(A, less, great);
dualQuickSort(A, great + 1, right);
}
public void swap(int[] A, int i, int j) {
int t = A[i];
A[i] = A[j];
A[j] = t;
}
public static void main(String[] args) {
int[] A = new int[]{13, 3, 65, 97, 76, 10, 35, 71, 5, 7, 3, 27, 49};
System.out.println(Arrays.toString(A));
Test dualQuickSort = new Test();
int l = 0;
int r = A.length - 1;
dualQuickSort.dualQuickSort(A, l, r);
System.out.println(Arrays.toString(A));
}
}
/*
输出
[13, 3, 65, 97, 76, 10, 35, 71, 5, 7, 3, 27, 49]
left :0 less 6 great7 right 12
[13, 3, 3, 7, 10, 5, 35, 27, 71, 76, 97, 65, 49]
left :0 less 3 great4 right 5
[5, 3, 3, 7, 10, 13, 35, 27, 71, 76, 97, 65, 49]
left :0 less 1 great1 right 2
[3, 3, 5, 7, 10, 13, 35, 27, 71, 76, 97, 65, 49]
left :3 less 4 great3 right 4
[3, 3, 5, 7, 10, 13, 35, 27, 71, 76, 97, 65, 49]
left :6 less 7 great6 right 7
[3, 3, 5, 7, 10, 13, 27, 35, 71, 76, 97, 65, 49]
outer
left :8 less 9 great9 right 12
[3, 3, 5, 7, 10, 13, 27, 35, 49, 65, 97, 76, 71]
left :10 less 11 great11 right 12
[3, 3, 5, 7, 10, 13, 27, 35, 49, 65, 71, 76, 97]
[3, 3, 5, 7, 10, 13, 27, 35, 49, 65, 71, 76, 97]
*/
c. DualQuickSort 性能分析
- 如果按照元素 比较次数 来比较的话,DualQuickSort 的元素比较次数其实比 QuickSort 要多;
- CPU 与内存速度是不匹配的,在过去的30年里面,CPU 的速度平均每年增长46%, 而内存的带宽每年只增长37%,那么经过30年的这种不均衡发展,它们之间的差距已经蛮大了。假如这种不均衡持续持续发展,有一天 CPU 速度再增长也不会让程序变得更快,因为 CPU 始终在等待内存传输数据,形成了内存墙(Memory Wall);
- 由于 CPU 与内存的发展失衡,我们在分析算法复杂性的时候已经不能简单地用元素比较次数来比较了,因为这种比较的方法只考虑了 CPU 的因素,没有考虑内存的因素。对于那种对输入数据进行顺序扫描的排序算法,扫描元素的个数这种新的算法把内存的流量的因素考虑了进去,更适合新时代:在这种新的算法里面,我们把对于数组里面一个元素的访问 “array[i]” 称为一次扫描。对数组同一个索引的多次访问在 “元素扫描次数” 的算法里面只算一次的(这个访问包括读和写)。扫描元素个数反应的是 CPU 与内存之间的数据流量的大小;
- 如果按照元素 元素扫描次数 来比较的话,QuickSort 确实进行了更多的元素扫描动作,因此也就比较慢。在这种新的算法里面,DualQuickSort 比 QuickSort 节省了12%的元素扫描,从实验来看,DualQuickSort 节省了10%的时间;
- 30年前 DualQuickSort 可能真的比 QuickSort 要慢,但是30年之后虽然算法还是以前的那个算法,但是计算机已经不是以前的计算机了。在现在的计算机里面 DualQuickSort 算法更快!
4. 结论
- 虽然 DualQuickSort 的元素比较次数比 QuickSort 要多,但是因为 DualQuickSort 比 QuickSort 节省了更多的元素扫描,从而节省了实际运算时间,故取代了 QuickSort 成为处理大量随机数据时的最佳方案。