Arrays.sort
先介绍一下背景知识:
基数排序/计数排序/桶排序 radix/base sort,counting sort:
1)适用情况:可以简单分为多少个“桶”,不仅是数字也可以是字符串其他分类等等
2)每个桶内部可以使用其他排序方法
3)特点:以空间换时间
关于快排:
1)基本有序时肯定不能用
2)规模较小时递归本身产生的开销太大,所以不建议使用
这也是规模小时不采用快排,以及实际应用中,快排和归并(都是递归实现)本身在规模小于一定阈值时,使用冒泡或插入排序来实现(即递归的底层是其他方式实现的)。
3)对象的排序要注意两点
① 不同于是纯数字,对稳定性(Stability)有要求
② 由于对象的比较操作开销比较大(要比置换赋值大得多),所以对于对象的排序最好使用比较次数比较少的排序方法。
https://www.cnblogs.com/gw811/archive/2012/10/04/2711746.html
言归正传,JDK1.6及之前Arrays.sort使用的都是传统的快排方法,即单轴快排,直接在Array类中给出排序实现,如其中核心方法sort1就是在sort方法中被调用的(话说这个命名也太随意了点。。。),中文是我加的注释:
private static void sort1(long x[], int off, int len) {
//① 递归底层使用插入排序
// 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;
}
// ② 采用中位数的方法选取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
}
long v = x[m];
// ③ 对选出来的pivot使用经典快排
// 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
// 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);
//递归排序左右两侧的数组
// Recursively sort non-partition-elements
if ((s = b-a) > 1)
sort1(x, off, s);
if ((s = d-c) > 1)
sort1(x, n-s, s);
}
// swap方法交换两个变量
/**
* Swaps x[a] with x[b].
*/
private static void swap(long x[], int a, int b) {
long t = x[a];
x[a] = x[b];
x[b] = t;
}
// vecswap方法交换两个向量
/**
* Swaps x[a .. (a+n-1)] with x[b .. (b+n-1)].
*/
private static void vecswap(long x[], int a, int b, int n) {
for (int i=0; i<n; i++, a++, b++)
swap(x, a, b);
}
// med3方法:返回三个数中的中位数
/**
* Returns the index of the median of the three indexed longs.
*/
private static int med3(long x[], int a, int b, int c) {
return (x[a] < x[b] ?
(x[b] < x[c] ? b : x[a] < x[c] ? c : a) :
(x[b] > x[c] ? b : x[a] > x[c] ? c : a));
}
简单分析一下:
① 在递归的底层(java 6选择的阈值是7),使用插入排序,return
② 数组长度大于等于7时,使用经典快排。
但是在选取pivot时不是选取固定位置的元素,而是使用中位数的方法,如下:
1)为什么使用中位数:快排在一直选取最小值或者最大值时性能最差,达到n^2,故中位数最为保险;
2)按数组长度区别处理有两个效果:
a.当输入的原始数组时,可直接进入对应处理情况,特别是处于(0,40]时,可直接进行处理。
b.当输入数组很大时,将会从(40,∞)开始递归,层层下降,下降到[7,40]时使用一次性中位数策略,下降到(0,7)时使用插入排序。其实可以明显看出1)是2)的子集。
③ 对于②中使用中位数策略选出来的pivot,开始进行经典快排
注意一下a,d的作用:只有当“x[b] == v”时才会向中间移动
即,将数组中所有和pivot相等的元素都放在了新数组的两边,然后将两侧pivot移入中间,如下例:
注意,vecswap下标计算这些细节一定要细心,记住vecswap的n为要平移的长度,但是是移动Swaps x[a .. (a+n-1)] with x[b .. (b+n-1)],x[a+n]和x[b+n]是没有移动的。
③ 最后递归排序左右两侧的子序列(利用之前的标记adcd很容易知道除去pivot之后的左右子序列)