以前总结过排序的种种知识
那么在Java中的Arrays.sort()是如何写的呢?
JDK5中的Arrays.sort(int[])
JDK5基本类型的排序是使用优化了的快速排序,我们来看看JDK5中的优化点
/**
* 将指定范围的整形数组升序排序。
* x[] 待排数组
* off 从数组的第off个元素开始排序
* len 数组长度
*/
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),直接插入排序反而要比快排,归并排序要好。
这个道理很简单。数组规模小,简单算法的比较次数不会比先进算法多多少。相反,诸如快排,归并排序等先进算法使用递归操作,所付出的运行代价更高。
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时使用插入排序)。
● 如果是中等规模数组(7
● 如果是大规模数组(size>40),则在9个指定的数中取一个伪中数(中间大小的数s)中小规模时,这种取法尽量可以避免数组的较小数或者较大数成为枢轴。值得一提的是大规模的时候,首先在数组中寻找9个数据(可以通过源代码发现这9个数据的位置较为平均的分布在整个数组上);然后每3个数据找中位数;最后在3个中位数上再找出一个中位数作为枢轴。
这种精心选择的枢轴,使得快排的最差情况成为了极小概率事件了。
代码中的v就是最终选择的pivot
3. 根据pivot v划分,形成一个形如 (v)* 的数组
// Establish Invariant: 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的优化细节 15、93、15、41、6、15、22、7、15、20
第一次pivot:v=15
阶段一,形成 v* (v)* v* 的数组:
15、15、 7、6、 41、20、22、93、 15、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相等的元素交换到数组中间的位置上
7、6、 15、15、 15、15、 41、20、22、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不相等都元素区间{7、6}和{41、20、22、93}
对于重复元素较多的数组,这种优化无疑能到达更好的效率。
JDK5中的Arrays.sort(Object[])
对象数组的排序,如Arrays.sort(Object[])等。采用了一种经过修改的归并排序 。
/**
* 将指定范围的对象数组按自然顺序升序排序。
* src[] 原待排数组
* dest[] 目的待排数组
* low 待排数组的下界位置
* high 待排数组的上界位置
* off 从数组的第off个元素开始排序
*/
1. 同上面的快速排序
// Insertion sort on smallest arrays
if (length < INSERTIONSORT_THRESHOLD) {
for (int i=low; 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比较,但是compare,equals之类的操作成本有可能会很大,因为有时计算hashcode将会产生很大成本。所以在对象的排序中,移动的成本远低于比较的成本。那么在排序算法的选择中,更加倾向于选择比较次数较少的归并排序。
参考资料:
1. http://hxraid.iteye.com/blog/665095
2. http://www.zhihu.com/question/24727766
3. http://blog.csdn.net/jy3161286/article/details/23361191?