Java源码分析——Arrays.sort()

    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)

    普通的快速排序并不能很好地适应各种情况,来看看源码中是如何优化的。这里会涉及到一些改进的快速排序算法,比如前文提到的“双轴快排”,可以参考:几种改进的快排算法。算法的步骤可以表示如下:

  1. 将数组平均分为7段,用e1, e2, e3, e4, e5 隔开,并且将e1, e2, e3, e4, e5这五个数用插入排序算法排序,确保e1, e2, e3, e4, e5这五个数从小到大排列;
  2. 如果e1, e2, e3, e4, e5都不相等的话,选e2, e4这两个作为基准,采用上述链接中的“双轴快速排序算法”(还是有区别的,注意基准元素不一样,思想是这个思想),然后对两头的的元素进行递归排序,注意这里对中间段的元素进行了判断,如果中间元素不是很多,就跟两头的处理方法一样:进行递归;如果中间元素过多(less < e1 && e5 < great),则认为有很多相等的元素,再进行一次机遇两个基准的双轴快排;
  3. 如果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源码中还有很多其它的方法,后续跟进。

 

 

 

 

 

 

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值