JDK8中Arrays.java这个类的代码咋一看很多,有5000多行。但是,里面有很多方法把基本类型(有的方法Boolean类型除外)全都实现了一遍。例如下面的排序算法sort。只要看懂了一个,其它的也就懂了,基本上是一样的,个别地方有差别。这样看下来,其实Array源码真正的内容就少了许多了。
public static void sort(int[] a){...}
public static void sort(int[] a, int fromIndex, int toIndex){...}
public static void sort(long[] a){...}
public static void sort(long[] a, int fromIndex, int toIndex){...}
public static void sort(short[] a){...}
public static void sort(short[] a, int fromIndex, int toIndex){...}
public static void sort(char[] a){...}
public static void sort(char[] a, int fromIndex, int toIndex){...}
public static void sort(byte[] a){...}
public static void sort(byte[] a, int fromIndex, int toIndex){...}
public static void sort(float[] a){...}
public static void sort(float[] a, int fromIndex, int toIndex){...}
public static void sort(double[] a){...}
public static void sort(double[] a, int fromIndex, int toIndex){...}
上面每种基本类型都有两个排序函数,从参数很明显可以看出来,一个是对整个数组进行排序,另一个是对数组中的某个范围的内容进行排序。下面以 int 类型为例,对Arrays.sort()进行简单分析一下。首先上代码:
public static void sort(int[] a) {
DualPivotQuicksort.sort(a, 0, a.length - 1, null, 0, 0);
}
可以看到,函数体里面只是调用了 DualPivotQuicksort 类的sort函数,DualPivotQuicksort 网上翻译为“双轴快排”,暂且先这么称呼,后面会看到为什么叫“双轴”。继续一层层往下拨,会发现在这个sort函数里面做了很多优化。因为要考虑到适应不同大小,不同数据分布的数组排序,做了针对数组大小不同用不同的排序方法的处理。具体如下:
- 当数组 length >= QUICKSORT_THRESHOLD 的时候采用归并排序(QUICKSORT_THRESHOLD为类中定义的一个常量,值为286)
- 当数组 length >= INSERTION_SORT_THRESHOLD && length < QUICKSORT_THRESHOLD 的时候采用快速排序(INSERTION_SORT_THRESHOLD 也是类中定义的一个常量,值为47)
- 当数组 length < INSERTION_SORT_THRESHOLD 的时候采用插入排序
为什么针对 length 的不同采用不同的排序算法,无非是每一种算法都只是在一定的范围内展现出其优越性,没有哪一种算法是可以在所有范围内都表现出它的先进性的。这个具体的分析这里不讨论了(其实是还没有做具体分析,先不写了。看有的书上是这么写的:研究表明,序列长度M取值为5~25时,采用直接插入排序要比快排至少快10%。那就先“研究表明”吧)。下面来看一看每种排序是如何进行的。
1. 插入排序(length < INSERTION_SORT_THRESHOLD)
插入排序根据待排序列是否处在整个数组的最左侧分两种情况,如果是:采用最普通的插入排序,这里就不贴代码了;如果不是:那么在普通插入排序的基础上做了优化。具体看代码:
// 跳过最开始的升序序列,把left定位在比前一个数小的位置上
do {
if (left >= right) {
return;
}
} while (a[++left] >= a[left - 1]);
// 从上面的定位处开始,一直往后
for (int k = left; ++left <= right; k = ++left) {
// 每次取两个,这两个数用插入的方法找到各自变成有序之后的位置,注意一个是 k+2,一个是 k+1
int a1 = a[k], a2 = a[left];
if (a1 < a2) {
a2 = a1; a1 = a[left];
}
while (a1 < a[--k]) {
a[k + 2] = a[k];
}
a[++k + 1] = a1;
while (a2 < a[--k]) {
a[k + 1] = a[k];
}
a[k + 1] = a2;
}
// 最后一个落单的单独处理
int last = a[right];
while (last < a[--right]) {
a[right + 1] = a[right];
}
a[right + 1] = last;
插入排序的性能消耗主要在每次插入时的大小比较,这里做了一个优化,每次取两个进行插入,在一定层度上减少了比较次数。
2. 快速排序(length >= INSERTION_SORT_THRESHOLD && length < QUICKSORT_THRESHOLD)
普通的快速排序并不能很好地适应各种情况,来看看源码中是如何优化的。这里会涉及到一些改进的快速排序算法,比如前文提到的“双轴快排”,可以参考:几种改进的快排算法。算法的步骤可以表示如下:
- 将数组平均分为7段,用e1, e2, e3, e4, e5 隔开,并且将e1, e2, e3, e4, e5这五个数用插入排序算法排序,确保e1, e2, e3, e4, e5这五个数从小到大排列;
- 如果e1, e2, e3, e4, e5都不相等的话,选e2, e4这两个作为基准,采用上述链接中的“双轴快速排序算法”(还是有区别的,注意基准元素不一样,思想是这个思想),然后对两头的的元素进行递归排序,注意这里对中间段的元素进行了判断,如果中间元素不是很多,就跟两头的处理方法一样:进行递归;如果中间元素过多(less < e1 && e5 < great),则认为有很多相等的元素,再进行一次机遇两个基准的双轴快排;
- 如果e1, e2, e3, e4, e5有相等的情况,则选取e3作为唯一的一个基准,采用上述链接中“三向切分的快速排序”思想进行排序
3. 归并排序(length >= QUICKSORT_THRESHOLD)
在做归并排序之前,先检测整个数组的有序情况,在这个检测的过程中,会把整个数组排成一段一段的局部有序非递减的序列(如果是碰到一段非递增序列,就将其顺序反过来),并统计整个数组中有多少个这样的局部有序的序列。
当这样的有序序列个数达到 MAX_RUN_COUNT 个时(MAX_RUN_COUNT是类中定义的常量,值为67),就认为整个数组是相对无序的(试想一下,有序序列个数越少,说明越有序,整体有序时,有序序列个数为1),这时并不采用归并排序,而是采用2中的快速排序算法进行排序。
当有序序列个数小于 MAX_RUN_COUNT时,才真正采用基于循环方法(不是递归)的归并排序。
以上是关于Arrays.sort()的简单介绍,写得很粗糙,很多细节都没有分析到,主要是想做一个整体的记录。另外,并不是所有的基本类型都是按照这样排序的。比如:char类型,byte类型和short类型,因为它们能表示的范围比较小,所以加入了“计数排序”,当要排序的序列长度大于一定的值的时候,char,short是3200,byte是29,采用计数排序,否则采用上面分析的sort()方法进行排序。
当然,Arrays.java源码中还有很多其它的方法,后续跟进。