前面涉及到了那么多时间复杂度分类的排序算法,冒泡排序、插入排序、选择排序;归并排序、快速排序; 桶排序、计数排序、基数排序,可能常用的排序可能还有堆排序等。了解了每种排序算法的最好最坏平均时间复杂度,空间复杂度(是否原地排序),是否稳定排序等,这些可以理解为都是理论,当我们真正落地使用时会使用哪些排序,现在分析一下Java中涉及的排序算法,进行源码分析。源码分析会比较散,看起来会比较乱,所以下面先进行整体总结再分析源码,方便把控。
Java中使用的排序方法主要有 Collections.sort、List.sort,而Collections.sort会调用List.sort, 再调到Arrays.sort。即Java的排序算法的真正实现是Arrays.sort,并且提供了两个重载方法是否传入比较器Comparator,虽然画图显示分成了多条路径,但是传入的Comparator本身仅仅是在具体排序算法的比较动作时使用【排序算法很重要的动作就是比较和交换,此处只是使用我们自己写的代码来进行比较动作】。比较器本身是@FunctionalInterface,即一个回调接口,即将我们写的代码与排序算法的比较动作解耦合。
排序方法会根据Arrays的内部类LegacyMergeSort的userRequested属性进行判断是否调用老的排序方法,其实就是对早起版本的兼容,后续版本进行删除。我查看的Jdk7/8的版本中还存在,我们可以在启动Java main方法是,通过设置启动参数java.util.Arrays.useLegacyMergeSort进行设置。
比较老的版本中会判断待排序数组的长度,如果小于7则直接插入排序,否则使用归并排序。
新版本中则会使用TimSort,其排序考虑到了待排序数组的有序度和逆序度子单元,具体的直接看看TimSort类的注释说明:TimSort是一种稳定的、自适应的、迭代的合并排序,它所需要的时间远远少于O(N*logN),在对随机数组排序时与传统的归并排序有相当的性能提升。与传统的归并排序相比其最坏时间复杂度还是O(N*logN),只是最坏空间复杂度是O(N/2),最佳情况下只占用少量的固定空间【传统归并排序的空间复杂度是O(N)】。java的TimSort改编自Tim peters的Python版本。数据量比较大(长度大于32)时使用了优化的归并排序算法,否则会使用二分插入排序算法。
总结:比较老的排序版本已经废弃,所以java排序工具调用时都会使用TimSort进行排序。当长度小于32时会使用二分插入排序,否则会使用优化过的归并排序,其时间复杂度远小于O(N*logN),空间复杂度也基本只是传统归并排序的一半。
开始梳理源码:入口 Collections.sort, Jdk版本8.
public static <T extends Comparable<? super T>> void sort(List<T> list) {
list.sort(null); // 调的是list本身的方法
}
default void sort(Comparator<? super E> c) {
Object[] a = this.toArray();
// 调用Arrays.sort进行排序
Arrays.sort(a, (Comparator) c);
// 最后将排序结构赋值给原对象
ListIterator<E> i = this.listIterator();
for (Object e : a) {
i.next();
i.set((E) e);
}
}
所以实现的入口都是Arrays的sort方法:根据是否传入比较器分成了两条路径,其实最终会回到一条线,比较器只是一个回调函数,在具体排序算法的比较、交换的比较阶段回调。
public static <T> void sort(T[] a, Comparator<? super T> c) {
if (c == null) {
// 具体的调用在下面的方法,也是会先判断是否调用早期的版本
sort(a);
} else {
if (Arrays.LegacyMergeSort.userRequested)
legacyMergeSort(a, c);
else
TimSort.sort(a, 0, a.length, c, null, 0, 0);
}
}
public static void sort(Object[] a) {
if (Arrays.LegacyMergeSort.userRequested)
legacyMergeSort(a);
else
ComparableTimSort.sort(a, 0, a.length, null, 0, 0);
}
1、早期版本,长度小于7时使用传统的插入排序,否则使用传统的归并排序
private static void legacyMergeSort(Object[] a) {
Object[] aux = a.clone();
mergeSort(aux, a, 0, a.length, 0);
}
// 当数据长度小于7时使用传统插入排序,否则使用传统归并排序
private static void mergeSort(Object[] src, Object[] dest, int low, int high, int off) {
int length = high - low;
// 当数据规模小时使用插入排序,INSERTIONSORT_THRESHOLD = 7
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;
}
// Recursively sort halves of dest into src
int destLow = low;
int destHigh = high;
low += 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++];
}
}
2、否则调用ComparableTimSort或者TimSort类型进行排序(类似),所以只看 TimSort.sort 方法:长度小于32时使用,mini-TimSort【一种优化的二分插入排序算法】,否则使用优化的归并排序算法。
static <T> void sort(T[] a, int lo, int hi, Comparator<? super T> c,
T[] work, int workBase, int workLen) {
assert c != null && a != null && lo >= 0 && lo <= hi && hi <= a.length;
int nRemaining = hi - lo;
if (nRemaining < 2) // 长度小于2不用进行排序
return;
//如果数据长度小于32,使用"mini-TimSort"进行排序,可以认为是二分插入排序的优化
if (nRemaining < MIN_MERGE) {
int initRunLen = countRunAndMakeAscending(a, lo, hi, c);
binarySort(a, lo, hi, lo + initRunLen, c);
return;
}
// 使用TimSort 优化的归并排序,归并本身就是分治和合并的过程,刚好符合栈的特性
TimSort<T> ts = new TimSort<>(a, c, work, workBase, workLen);
int minRun = minRunLength(nRemaining);
do {
// Identify next run
int runLen = countRunAndMakeAscending(a, lo, hi, c);
// If run is short, extend to min(minRun, nRemaining)
if (runLen < minRun) {
int force = nRemaining <= minRun ? nRemaining : minRun;
binarySort(a, lo, lo + force, lo + runLen, c);
runLen = force;
}
// Push run onto pending-run stack, and maybe merge
ts.pushRun(lo, runLen);
ts.mergeCollapse();
// Advance to find next run
lo += runLen;
nRemaining -= runLen;
} while (nRemaining != 0);
// Merge all remaining runs to complete sort
assert lo == hi;
ts.mergeForceCollapse();
assert ts.stackSize == 1;
}