Java中的排序

之前总结过排序的种种知识 java

那么在Java中的Arrays.sort()是如何写的呢? 算法

JDK5中的Arrays.sort(int[])

JDK5基本类型的排序是使用优化了的快速排序,咱们来看看JDK5中的优化点 数组

  1. /** 
  2.     * 将指定范围的整形数组升序排序。 
  3.     * x[] 待排数组 
  4.     * off 从数组的第off个元素开始排序 
  5.     * len 数组长度 
  6.     */  

1. 在小规模(size<7)数组中,直接插入排序的效率要比快速排序高

// Insertion sort on smallest arrays
if (len < 7)
{
    for (int i = off; i < len + off; i++)
    for (int j = i; j > off && x[j - 1] > x[j]; j--)
            swap(x, j, j - 1);
    return;
		}

没有一种排序在任何状况下都是最优的。O(N^2)级别的排序看起来彷佛比全部先进排序要差的多。但实际上也并不是如此,Arrays中的sort()算法就给了咱们一个很好的例子。当待排数组规模很是小的时候(JDK中规模的阈值为INSERTIONSORT_THRESHOLD=7),直接插入排序反而要比快排,归并排序要好。 数据结构

这个道理很简单。数组规模小,简单算法的比较次数不会比先进算法多多少。相反,诸如快排,归并排序等先进算法使用递归操做,所付出的运行代价更高。 dom

2. 精心选择划分元素,即pivot

// Choose a partition element, v
int m = off + (len >> 1); // Small arrays, middle element
if (len > 7)
{
    int l = off;
    int n = off + len - 1;
    if (len > 40)
    { // Big arrays, pseudomedian of 9
        int s = len / 8;
        l = med3(x, l, l + s, l + 2 * s);
        m = med3(x, m - s, m, m + s);
        n = med3(x, n - 2 * s, n - s, n);
    }
    m = med3(x, l, m, n); // Mid-size, med of 3
}
int v = x[m];

快排有一种最差的状况,即蜕化成效率最差的冒泡排序。 缘由请查看这里快排相关知识。 性能

既然如此,咱们能够看看JDK5中的Arrays.sort()是如何为咱们选择pivot的。 优化

● 若是是小规模数组(size==7),直接取中间元素做为pivot(<7时使用插入排序)。       ui

● 若是是中等规模数组(7<size<=40),则在数组首、中、尾三个位置上的数中取中间大小的数做为pivot this

● 若是是大规模数组(size>40),则在9个指定的数中取一个伪中数(中间大小的数s)中小规模时,这种取法尽可能能够避免数组的较小数或者较大数成为枢轴。值得一提的是大规模的时候,首先在数组中寻找9个数据(能够经过源代码发现这9个数据的位置较为平均的分布在整个数组上);而后每3个数据找中位数;最后在3个中位数上再找出一个中位数做为枢轴。 spa

这种精心选择的枢轴,使得快排的最差状况成为了极小几率事件了。

代码中的v就是最终选择的pivot

3. 根据pivot v划分,造成一个形如  (<v)*   v* (>v)* 的数组

// Establish Invariant: v* (<v)* (>v)* v*
int a = off, b = a, c = off + len - 1, d = c;
while (true)
{
    while (b <= c && x[b] <= v)
    {
        if (x[b] == v)
        swap(x, a++, b);
        b++;
    }
    while (c >= b && x[c] >= v)
    {
        if (x[c] == v)
        swap(x, c, d--);
        c--;
    }
    if (b > c)
        break;
    swap(x, b++, c--);
}

普通快排算法,都是使得pivot移动到数组的较中间位置。pivot以前的元素所有小于或等于pivot,以后的元素所有大于pivot。但与pivot相等的元素并不能移动到pivot附近位置。这一点在Arrays.sort()算法中有很大的优化。

解释下上述代码的变量的意思,a表明着左边和pivot相等的数应该交换的位置,同理d表明着右边和pivot相等的数应该交换的位置(因此最后和pivot相等的数字会集中在两边),b就是咱们一般快排中的i指针,c就是j指针。

咱们举个例子来讲明Arrays的优化细节  1五、9三、1五、4一、六、1五、2二、七、1五、20

第一次pivot:v=15

阶段一,造成 v* (<v)* (>v)* v* 的数组:

                                          1五、1五、 七、六、 4一、20、2二、9三、 1五、15

咱们发现,与pivot相等的元素都移动到了数组的两边。而比pivot小的元素和比pivot大的元素也都区分开来了。

// Swap partition elements back to middle
    int s, n = off + len;
    s = Math.min(a - off, b - a);
    vecswap(x, off, b - s, s);
    s = Math.min(d - c, n - d - 1);
    vecswap(x, b, n - s, s);

阶段二,将pivot和与pivot相等的元素交换到数组中间的位置上

                                          七、六、 1五、1五、 1五、1五、 4一、20、2二、93

// Recursively sort non-partition-elements
    if ((s = b - a) > 1)
        sort1(x, off, s);
    if ((s = d - c) > 1)
        sort1(x, n - s, s);

阶段三,递归排序与pivot不相等都元素区间{七、6}和{4一、20、2二、93}

对于重复元素较多的数组,这种优化无疑能到达更好的效率。

JDK5中的Arrays.sort(Object[])

对象数组的排序,如Arrays.sort(Object[])等。采用了一种通过修改的归并排序 。

  1.   /** 
  2.     * 将指定范围的对象数组按天然顺序升序排序。 
  3.     * src[] 原待排数组 
  4.     * dest[] 目的待排数组 
  5.     * low 待排数组的下界位置 
  6.     * high 待排数组的上界位置 
  7.     * off 从数组的第off个元素开始排序 
  8.     */  

1. 同上面的快速排序

// Insertion sort on smallest arrays
if (length < INSERTIONSORT_THRESHOLD) {
    for (int i=low; i<high; i++)
    for (int j=i; j>low &&((Comparable) dest[j-1]).compareTo(dest[j])>0; j--)
        swap(dest, j, j-1);
    return;
}
如同上面的快速排序同样,当排序规模小于7时,插入排序的效率反而比归并排序高,缘由同上。

2. 若是低子列表中的最高元素小于高子列表中的最低元素,则忽略合并

       // Recursively sort halves of dest into src
        int destLow  = low;
        int destHigh = high;
        low  += off;//不用去看off变量,off的做用是,排序部分的位置。
        high += off;
        int mid = (low + high) >> 1;
        mergeSort(dest, src, low, mid, -off);
        mergeSort(dest, src, mid, high, -off);

        // If list is already sorted, just copy from src to dest.  This is an
        // optimization that results in faster sorts for nearly ordered lists.
        if (((Comparable)src[mid-1]).compareTo(src[mid]) <= 0) {
            System.arraycopy(src, low, dest, destLow, length);
            return;
        }

        // Merge sorted halves (now in src) into dest
        for(int i = destLow, p = low, q = mid; i < destHigh; i++) {
            if (q >= high || p < mid && ((Comparable)src[p]).compareTo(src[q])<=0)
                dest[i] = src[p++];
            else
                dest[i] = src[q++];
        }

这个优化措施无疑对基本有序序列是极大的效率改进。

这两个优化都很简单,实际上效率并未提升多少。因此在JDK7中将其替换为TimSort

JDK7中的Arrays.sort(int[])

DualPivotQuicksort是JDK1.7开始的采用的快速排序算法。

通常的快速排序采用一个枢轴来把一个数组划分红两半,而后递归之。

大量经验数据表面,采用两个枢轴来划分红3份的算法更高效,这就是DualPivotQuicksort。

DualPivotQuicksort流程图:


接下来,咱们经过源码一步步得查看jdk7中是如何作的。

// Use Quicksort on small arrays
        if (right - left < QUICKSORT_THRESHOLD) {
            sort(a, left, right, true);
            return;
        }
当数据规模小于286时,才使用快排,大于等于286时,将使用TimSort,这咱们接下来说,咱们先看看这里的快排是如何写的。

// Use insertion sort on tiny arrays
  if (length < INSERTION_SORT_THRESHOLD) {
           ...
  }
这JDK5同样,当规模小于47时使用插入排序(JDK5中是小于7),缘由同JDK5中所说的。
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 + 1] = ai;
    }
}

leftmost表明该区间是不是数组中最左边的区间。举个例子:

  数组:[2, 4, 8, 5, 6, 3, 0, -3, 9]能够分红三个区间(2, 4, 8){5, 6}<3, 0, -3, 9>

  对于()区间,left=0, right=2, leftmost=true

  对于 {}区间, left=3, right=4, leftmost=false,同理可得<>区间的相应参数

当leftmost为true时,它会采用传统的插入排序(traditional insertion sort),代码也较简单,其过程解析查看这篇 blog中的插入排序章节。
else {
/*
 * Skip the longest ascending sequence.
 */
    do {
        if (left >= right) {
            return;
        }
    } while (a[++left] >= a[left - 1]);

/*
 * Every element from adjoining part plays the role
 * of sentinel, therefore this allows us to avoid the
 * left range check on each iteration. Moreover, we use
 * the more optimized algorithm, so called pair insertion
 * sort, which is faster (in the context of Quicksort)
 * than traditional implementation of insertion sort.
 */
    for (int k = left; ++left <= right; k = ++left) {
        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;
}
当leftmost为false时,它采用一种新型的插入排序(pair insertion sort),改进之处在于每次遍历前面已排好序的数组须要插入两个元素,而传统插入排序在遍历过程当中只须要为一个元素找到合适的位置插入。对于插入排序来说,其关键在于为待插入元素找到合适的插入位置,为了找到这个位置,须要遍历以前已经排好序的子数组,因此对于插入排序来说,整个排序过程当中其遍历的元素个数决定了它的性能。很显然,每次遍历插入两个元素能够减小排序过程当中遍历的元素个数。
为左边区间时,pair insertion sort在左边元素比较大时,会越界。


总结:

赛德维克在红色的《算法》里讲过这样一段话:

Java的标准库应该是对抽象类型的数据结构使用归并排序的一种变种,而对于基本类型采起三向切分的快排变种。

那么为何对于基本类型使用快排,而对于抽象类型则使用归并呢?

1. 归并排序稳定,快速排序不稳定。

对于对象排序而言,稳定性是很重要的,一个对象每每有多个属性,假如两个对象的compare值是对等的,可是排序事后相互顺序却变了,是看得出来的,并且在内存上是有意义的。

2. compare的成本

对象不能用><=符号去比较,要使用compare,equals比较,可是compareequals之类的操做成本有可能会很大,由于有时计算hashcode将会产生很大成本。因此在对象的排序中,移动的成本远低于比较的成本。那么在排序算法的选择中,更加倾向于选择比较次数较少的归并排序。


参考资料:

1. http://hxraid.iteye.com/blog/665095

2. http://www.zhihu.com/question/24727766

3. http://blog.csdn.net/jy3161286/article/details/23361191?

相关文章
相关标签/搜索