Java双基准快速排序算法源码阅读
jdk版本:jdk1.8.0_191
Java数组和List对象的排序方法分别是Arrays.sort(T[] a)和Collections.sort(List<T> list),分别位于java.util.Arrays和java.util.Collections类中. 由于List底层采用数组实现,因此Collections.sort方法也是通过调用Arrays.sort方法实现的。
Arrays.sort方法源码很简单,就是调用了DualPivotQuicksort.sort方法。
/**
* 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);
}
简单来说调用关系就是:
DualPivotQuicksort.sort -----> Arrays.sort -----> Collections.sort
于是再跟进去看DualPivotQuicksort.sort方法,位于java.util.DualPivotQuicksort类。从java7开始,针对排序方法的底层算法做了改进,从快速排序(Quicksort)改进为双基准快速排序(DualPivotQuicksort). DualPivotQuicksort,顾名思义,就是用2个pivot把一个待排序的数组分成3段,对这3段分别进行递归调用。关于DualPivotQuicksort相对于Quicksort能够提升速度的原因,这篇博文给出了很详尽的解释:新的快速排序算法: 《Dual-Pivot QuickSort》阅读笔记.
以int型数组为例,DualPivotQuickSort类中直接被Arrays.sort调用的方法的函数签名是
/**
* Sorts the specified range of the array using the given
* workspace array slice if possible for merging
*
* @param a the array to be sorted
* @param left the index of the first element, inclusive, to be sorted
* @param right the index of the last element, inclusive, to be sorted
* @param work a workspace array (slice)
* @param workBase origin of usable space in work array
* @param workLen usable size of work array
*/
static void sort(int[] a, int left, int right,
int[] work, int workBase, int workLen)
该default权限方法调用的是另一个同名的private方法sort(int[] a, int left, int right, boolean leftmost),它是双基准快速排序的算法实现,其方法注释如下:
/**
* Sorts the specified range of the array by Dual-Pivot Quicksort.
*
* @param a the array to be sorted
* @param left the index of the first element, inclusive, to be sorted
* @param right the index of the last element, inclusive, to be sorted
* @param leftmost indicates if this part is the leftmost in the range
*/
private static void sort(int[] a, int left, int right, boolean leftmost)
首先,属性INSERTION_SORT_THRESHOLD规定了使用插入排序/递归式快速排序的阈值,在这里阈值取为47. 插入排序是O(n2)的算法,而快速排序是平均复杂度O(nlogn)、最坏复杂度O(n2)的算法,但是由于快速排序的实现使用了递归,有函数调用的开销,因此在n特别小的时候反而不如复杂度为O(n^2)的非递归算法。
待排序的数组长度小于47时,就会使用插入排序。根据leftmost参数(待排序的分段是否是数组最左边的分段),对插入排序进行了优化,使用了所谓的pair insertion sort,具体可以看代码。
待排序的数组长度大于47时,就会使用递归式的双基准快速排序。双基准排序的思想如下图所示:
/*
* 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.
*/
首先从选取5个备选基准:e1, e2, e3, e4, e5,分别近似位于数组待排序部分的3/14, 5/14, 7/14, 9/14, 11/14, 用插入排序将e1, e2, e3, e4, e5升序排列,选取5/14位置和9/14位置的两个备选基准作为两个基准: pivot1, pivot2。通过3个指针less, great和k,将数组待排序部分分成3段:最左边< pivot1的一段、中间>= pivot1 && <= pivot2的一段、最右边 > pivot2的一段,然后递归调用3次分别将这3段数组排序。
最后附上算法实现的完整源码。
附录:java.util.DualPivotQuicksort.sort(int[] a, int left, int right, boolean leftmost)方法完整源码
/**
* Sorts the specified range of the array by Dual-Pivot Quicksort.
*
* @param a the array to be sorted
* @param left the index of the first element, inclusive, to be sorted
* @param right the index of the last element, inclusive, to be sorted
* @param leftmost indicates if this part is the leftmost in the range
*/
private static void sort(int[] a, int left, int right, boolean leftmost) {
int length = right - left + 1;
// Use insertion sort on tiny arrays
if (length < INSERTION_SORT_THRESHOLD) {
if (leftmost) {
/*
* Traditional (without sentinel) insertion sort,
* optimized for server VM, is used in case of
* the leftmost part.
*/
for (int i = left, j = i; i < right; j = ++i) {
int ai = a[i + 1];
while (ai < a[j]) {
a[j + 1] = a[j];
if (j-- == left) {
break;
}
}
a[j +