注:本文的JDK版本为13
Java中为内置类型提供了排序算法,具体调用很简单,假如有一个int
数组a
,那么直接调用
Arrays.sort(a);
就能对数组完成排序,其余的内置类型也是如此。
Java有个命名习惯或者说是规范,后面加s的都是工具类,比如Arrays、Collections、Executors等等
那么,这个排序算法背后究竟干了哪些事?在时间上又是如何优化的呢?带着这个疑问,我进一步追了源码阅读。
/**
* 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);
}
方法注释清清楚楚写了,在那些可能使其他快速排序降低性能的大部分数据集上,双轴快排仍然能够保证o(nlogn)
的时间复杂度。在这里注明,该方法会默认进行升序排序。
在java.util
包下,DualPivotQuicksort
类就实现了双轴快排算法,该类的Structure如下所示:
可以看到,最关键的方法就是sort()
方法,我们以int
数组为例,来进一步看该方法的源码。一百多行的代码比较长,为了方便阅读,我将其中的注释翻译成了中文。
/**
* 情况允许的时候,使用给定的工作区数组切片对数组的指定范围进行排序以进行合并
*
* @param a 传入的数组
* @param left 要排序的第一个元素索引(包含)
* @param right 要排序的最后一个元素索引(包含)
* @param work 工作区数组(切片)
* @param workBase 工作数组中可用空间的来源
* @param workLen 工作数组的可用大小
*/
static void sort(int[] a, int left, int right,
int[] work, int workBase, int workLen) {
// 如果是小数组,直接快排
if (right - left < QUICKSORT_THRESHOLD) {
sort(a, left, right, true);
return;
}
/*
* 索引 run[i] 是第 i 次运行的开始
*/
int[] run = new int[MAX_RUN_COUNT + 1];
int count = 0; run[0] = left;
// 检查数组是否几乎是有序的
for (int k = left; k < right; run[count] = k) {
// 首先排除数组开头的相同元素
while (k < right && a[k] == a[k + 1])
k++;
if (k == right) break; // 数组元素全部相同,那么检查完毕跳出for循环
if (a[k] < a[k + 1]) { // 往后找升序的数组切片
while (++k <= right && a[k - 1] <= a[k]);
} else if (a[k] > a[k + 1]) { // 往后找降序的数组切片
while (++k <= right && a[k - 1] >= a[k]);
// 转换降序数组切片为升序
for (int lo = run[count] - 1, hi = k; ++lo < --hi; ) {
int t = a[lo]; a[lo] = a[hi]; a[hi] = t;
}
}
// 合并转换后的降序数组到升序数组上
if (run[count] > left && a[run[count]] >= a[run[count] - 1]) {
count--;
}
/*
* 而如果这样的升序序列太多,那么说明数组十分无序,直接快排
*/
if (++count == MAX_RUN_COUNT) {
sort(a, left, right, true);
return;
}
}
// 下列条件应该成立:
// run[0] = 0
// run[<last>] = right + 1;(结束条件)
if (count == 0) {
// 数组已经有序
return;
} else if (count == 1 && run[count] > right) {
// 单个升序或降序的数组切片
// 前面提到了,run[<last>] = right + 1是结束条件,因此也可以直接返回
return;
}
right++;
if (run[count] < right) {
// 这是一种特殊情况,最后一个升序序列刚好延申到数组结束,或最后只剩一个元素
run[++count] = right;
}
// 定义合并的交替基准
byte odd = 0;
for (int n = 1; (n <<= 1) < count; odd ^= 1);
// 使用或创建临时数组 b 进行合并
int[] b;
int ao, bo; // 数组左偏移
int blen = right - left; // b数组的长度
if (work == null || workLen < blen || workBase + blen > work.length) {
work = new int[blen];
workBase = 0;
}
if (odd == 0) {
System.arraycopy(a, left, work, workBase, blen);
b = a;
bo = 0;
a = work;
ao = workBase - left;
} else {
b = work;
ao = 0;
bo = workBase - left;
}
// 合并
for (int last; count > 1; count = last) {
for (int k = (last = 0) + 2; k <= count; k += 2) {
int hi = run[k], mi = run[k - 1];
for (int i = run[k - 2], p = i, q = mi; i < hi; ++i) {
if (q >= hi || p < mi && a[p + ao] <= a[q + ao]) {
b[i + bo] = a[p++ + ao];
} else {
b[i + bo] = a[q++ + ao];
}
}
run[++last] = hi;
}
if ((count & 1) != 0) {
for (int i = right, lo = run[count - 1]; --i >= lo;
b[i + bo] = a[i + ao]
);
run[++last] = right;
}
int[] t = a; a = b; b = t;
int o = ao; ao = bo; bo = o;
}
}
首先就做了一个判断,如果是小数组,就直接快排:
// 如果是小数组,直接快排
if (right - left < QUICKSORT_THRESHOLD) {
sort(a, left, right, true);
return;
}
否则的话,源码中创建了一个run
数组。
int[] run = new int[MAX_RUN_COUNT + 1];
int count = 0; run[0] = left;
这个数组的含义不难理解,只需要看MAX_RUN_COUNT
变量的注释含义即可,可以看出是和合并数组有关的,具体作用还需要结合后面的代码来理解。
/**
* The maximum number of runs in merge sort.
*/
private static final int MAX_RUN_COUNT = 67;
接下来,源码中做了一个检查,来看看这个数组是否几乎是有序的。
// 检查数组是否几乎是有序的
for (int k = left; k < right; run[count] = k) {
// 首先排除数组开头的相同元素
while (k < right && a[k] == a[k + 1])
k++;
if (k == right) break; // 数组元素全部相同,那么检查完毕跳出for循环
if (a[k] < a[k + 1]) { // 往后找升序的数组切片
while (++k <= right && a[k - 1] <= a[k]);
} else if (a[k] > a[k + 1]) { // 往后找降序的数组切片
while (++k <= right && a[k - 1] >= a[k]);
// 转换降序数组切片为升序
for (int lo = run[count] - 1, hi = k; ++lo < --hi; ) {
int t = a[lo]; a[lo] = a[hi]; a[hi] = t;
}
}
// 合并转换后的降序到升序数组上(只是一个记录索引的过程,并不在这里真正合并)
// 不要忘记for循环中的run[count] = k语句,应该是在这里更新run数组来记录索引
if (run[count] > left && a[run[count]] >= a[run[count] - 1]) {
count--;
}
/*
* 而如果这样的升序序列太多,那么说明数组十分无序,直接快排
*/
if (++count == MAX_RUN_COUNT) {
sort(a, left, right, true);
return;
}
}
个人理解的过程是这样,找到一段一段升序或降序的数组,然后每次都记录索引到run
数组中,当run
数组满了,也就是说明数组太过于无序,这个时候应该直接采用快排。
关于为什么直接采用快排,这里还需要了解一下快排与归并排序的一点区别,在数组无序时,快排要表现得比归并出色
如果能够跳出循环,就代表数组较为有序,我们继续看源码是如何处理的。首先是做了一系列的判断处理:
// 下列条件应该成立:
// run[0] = 0
// run[<last>] = right + 1;(结束条件)
if (count == 0) {
// 数组已经有序
return;
} else if (count == 1 && run[count] > right) {
// 单个升序或降序的数组切片
// 前面提到了,run[<last>] = right + 1是结束条件,因此也可以直接返回
return;
}
right++;
if (run[count] < right) {
// 这是一种特殊情况,最后一个升序序列刚好延申到数组结束,或最后只剩一个元素
run[++count] = right;
}
这里要记住,run
数组记录的是递增的序列片段索引,比如下列初始化的数组
int[] test = new int[300];
for (int i = 0; i < 300; i++) {
test[i] = i % 30;
}
这个数组每隔30个元素就递增,那么run
数组就应该是[0, 30, 60, 90, 120, 150, 180, 210, 240, 270, 300, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
,处理完之后,首先申请额外数组空间,以便之后的排序。
// 定义合并的交替基准
byte odd = 0;
for (int n = 1; (n <<= 1) < count; odd ^= 1);
// 使用或创建临时数组 b 进行合并
int[] b;
int ao, bo; // 数组左偏移
int blen = right - left; // b数组的长度
if (work == null || workLen < blen || workBase + blen > work.length) {
work = new int[blen];
workBase = 0;
}
if (odd == 0) {
System.arraycopy(a, left, work, workBase, blen);
b = a;
bo = 0;
a = work;
ao = workBase - left;
} else {
b = work;
ao = 0;
bo = workBase - left;
}
再申请好工作数组之后,就是需要根据归并的思想,从run
数组中取出一段段递增序列然后不断合并到工作数组中,最后再将合并后的数组赋值给原数组,就完成了排序。
for (int last; count > 1; count = last) {
for (int k = (last = 0) + 2; k <= count; k += 2) {
int hi = run[k], mi = run[k - 1];
for (int i = run[k - 2], p = i, q = mi; i < hi; ++i) {
if (q >= hi || p < mi && a[p + ao] <= a[q + ao]) {
b[i + bo] = a[p++ + ao];
} else {
b[i + bo] = a[q++ + ao];
}
}
run[++last] = hi;
}
if ((count & 1) != 0) {
for (int i = right, lo = run[count - 1]; --i >= lo;
b[i + bo] = a[i + ao]
);
run[++last] = right;
}
int[] t = a; a = b; b = t;
int o = ao; ao = bo; bo = o;
}
至此,这个方法就结束了,不过上面只是分析了归并排序的情况,之前提到过,如果是小数组或者数组太过无序,那么就直接快排,这里的快排与普通快排不同,采用了双轴快排的思想。
双轴快排的思想是再快排的基础上,取最大索引和最小索引上的值,根据这两个值将数组分为三个部分,然后再对三个部分进行双轴快排,具体细节与源码就不在本文中展开了。