JDK1.8源码分析【排序】timsort
如无特殊说明,文中的代码均是JDK 1.8版本。
在JDK集合框架中描述过,JDK存储一组Object的集合框架是Collection。而针对Collection框架的一组操作集合体是Collections,里面包含了多种针对Collection的操作,例如:排序、查找、交换、反转、复制等。
这一节讲述Collections的排序操作。
public staticextends Comparable super T>> void sort(List list) { list.sort(null); }
Collections.sort方法调用的是List.sort方法,List.sort方法如下:
@SuppressWarnings({"unchecked", "rawtypes"}) default void sort(Comparator super E> c) { Object[] a = this.toArray(); Arrays.sort(a, (Comparator) c); // Arrays的排序方法 ListIteratori = this.listIterator(); for (Object e : a) { i.next(); i.set((E) e); } }
看到这里可能会觉得奇怪,List是接口,但为什么会有实现方法,这是JDK 1.8的新特性。具体特性描述请参考:Java 8接口有default method后是不是可以放弃抽象类了?
在List.sort方法实现中,排序使用的是Arrays#sort(T[], java.util.Comparator super T>)方法,所以Collections的sort操作最终也是使用Arrays#sort(T[], java.util.Comparator super T>)方法。
public staticvoid sort(T[] a, Comparator super T> c) { if (c == null) { sort(a); } else { if (LegacyMergeSort.userRequested) legacyMergeSort(a, c); else TimSort.sort(a, 0, a.length, c, null, 0, 0); } }
Arrays#sort(T[], java.util.Comparator super T>)方法使用了3种排序算法:
java.util.Arrays#legacyMergeSort | 归并排序,但可能会在新版本中废弃 |
java.util.ComparableTimSort#sort | 不使用自定义比较器的TimSort |
java.util.TimSort#sort | 使用自定义比较器的TimSort |
Arrays源码中有这么一段定义:
/** * Old merge sort implementation can be selected (for * compatibility with broken comparators) using a system property. * Cannot be a static boolean in the enclosing class due to * circular dependencies. To be removed in a future release. * / static final class LegacyMergeSort { private static final boolean userRequested = java.security.AccessController.doPrivileged( new sun.security.action.GetBooleanAction( “java.util.Arrays.useLegacyMergeSort” ))。booleanValue(); }
该定义描述是否使用LegacyMergeSort,即历史归并排序算法,默认为假,即不使用。所以Arrays.sort只会使用java.util.ComparableTimSort#排序或java.util.TimSort#排序,这两种方法的实现逻辑是一样的,只是java.util.TimSort#排序可以使用自定义的比较,而java.util.ComparableTimSort#排序不使用比较而已。
顺便补充一下,比较器是策略模式的一个完美又简洁的示例。总体来说,策略模式允许在程序执行时选择不同的算法。比如在排序时,传入不同的比较器(比较器),就采用不同的算法。
Timsort算法
Timsort是结合了合并排序(merge sort)和插入排序(插入排序)而得出的排序算法,它在现实中有很好的效率.Tim Peters在2002年设计了该算法并在Python中使用(TimSort)是Python中list.sort的默认实现)。该算法找到数据中已经排好序的块 - 分区,每一个分区叫一个run,然后按规则合并这些run.Pyhton自从2.3版以来一直采用Timsort算法排序, JDK 1.7开始也采用Timsort算法对数组排序。
Timsort的主要步骤:
判断数组的大小,小于32使用二分插入排序
static void sort(Object [] a,int lo,int hi,Object [] work,int workBase,int workLen){ // 检查lo,hi的的准确性 断言 a!= null && lo> = 0 && lo < = hi && hi <= a.length; int nRemaining = hi - lo; // 当长度为0或1时永远都是已经排序状态 if(nRemaining <2 ) return ; // 总是排序大小为0和1的数组 // 数组个数小于32的时候 // 如果阵列小,做一个“小TimSort”,没有合并 如果(nRemaining < MIN_MERGE){ // 找出连续升序的最大个数 INT initRunLen = countRunAndMakeAscending(一,瞧,嗨); // 二分插入排序 binarySort(a,lo,hi,lo + initRunLen); 回归; } // 数组个数大于32的时候 ......
找出最大的递增或者递减的个数,如果递减,则此段数组严格反一下方向
private static int countRunAndMakeAscending(Object [] a,int lo,int hi){ assert lo < hi; int runHi = lo + 1 ; if(runHi == hi) 返回 1 ; // 查找运行结束, 如果(((可比较)a [runHi ++])则降序反转范围 .compareTo(a [lo])<0){ // 降序递减 而(runHi) runHi ++ ; // 调整顺序 reverseRange(a,lo,runHi); } else { // 升序递增 而(runHi= 0) runHi ++ ; } return runHi - lo; }
在使用二分查找位置,进行插入排序。开始之前为全部递增数组,从开始+ 1开始进行插入,插入位置使用二分法查找。最后根据移动的个数使用不同的移动方法。
private static void binarySort(Object [] a,int lo,int hi,int start){ assert lo <= start && start <= hi; if(start == lo) 开始 ++ ; for(; start){ 可比较的枢轴 = (可比较)a [开始]; // 将左(和右)设置为[start](pivot)所属的索引 int left = lo; int right = start; 断言左<= 右; / * *不变量: * pivot> = all in [lo,left]。 * pivot* / while(左< 右){ int mid =(左+右)>>> 1 ; if(pivot.compareTo(a [mid])<0 ) 对 = 中; 否则 左 =中+ 1 ; } 断言左== 右; / * *不变量仍然保持:pivot> = all in [lo,left]和 * pivot 注意 *如果有等于枢轴的元素,则左边指向 *他们之后的第一个位置 - 这就是为什么这种稳定。 *将元件滑过以为枢轴腾出空间。 * / int n =开始 - 左; // 要移动的元素数量要移动的个数 // 在默认情况下,Switch只是arraycopy的优化 // 移动的方法 switch (n){ case 2:a [left + 2] = a [left + 1 ]。 案例 1:a [left + 1] = a [left]; 打破; // native复制数组方法 默认值:System.arraycopy(a,left,a,left + 1 ,n); } a [left] = pivot; } }
数组大小大于32时
数组大于32时,先算出一个合适的大小,在将输入按其升序和降序特点进行了分区。排序的输入的单位不是一个个单独的数字,而是一个个的块 - 分区。其中每一个分区叫一个run。针对这些运行序列,每次拿一个运行出来按规则进行合并。每次合并会将两个运行合并成一个运行。合并的结果保存到栈中。合并直到消耗掉所有的运行,这时将栈上剩余的run合并到只剩一个run为止。这时这个仅剩剩的跑便是排好序的结果。
static void sort(Object [] a,int lo,int hi,Object [] work,int workBase,int workLen){ // 数组个数小于32的时候 ...... // 数组个数大于32的时候 / ** *三月一次,从左到右,找到自然运行, *将短自然运行扩展到minRun元素,并合并运行 *保持堆栈不变。 * / ComparableTimSort ts = new ComparableTimSort(a,work,workBase,workLen); // 计算run的长度 int minRun = minRunLength(nRemaining); do { // 识别下一次运行 // 找出连续升序的最大个数 int runLen = countRunAndMakeAscending(a,lo,hi); // 如果run是short,则延伸到min(minRun,nRemaining) // 如果运行长度小于规定的minRun长度,先进行二分插入排序 if(runLen < minRun){ int force = nRemaining <= minRun?nRemaining:minRun; binarySort(a,lo,lo + force,lo + runLen); runLen = force; } //将run运行到pending-run stack,并且可能合并 ts.pushRun(lo,runLen); // 进行归并 ts.mergeCollapse(); // 提前找到下一个运行 lo + = runLen; nRemaining - = runLen; } while(nRemaining!= 0 ); // 合并所有剩余的运行以完成排序 断言 lo == hi; // 归并所有的run ts.mergeForceCollapse(); 断言 ts.stackSize == 1 ; }
1.计算出跑的最小的长度minRun
a)如果数组大小为2的N次幂,则返回16(MIN_MERGE / 2);
b)其他情况下,逐位向右位移(即除以2),直到找到介于16和32间的一个数;
/ ** *返回指定数组的最小可接受运行长度 * 长度。比这更短的自然运行将延长 * { @link #binarySort}。 * *粗略地说,计算是: * *如果n*否则如果n的精确幂为2,则返回MIN_MERGE / 2。 *否则返回一个int k,MIN_MERGE / 2 <= k <= MIN_MERGE,这样n / k *接近但严格小于2的精确幂。 * *有关基本原理,请参阅listsort.txt。 * * @param n要排序的数组的长度 * @return 要合并的最小运行的长度 * / private static int minRunLength(int n){ assert n> = 0 ; int r = 0; // 如果任何1位被移开, 则变为1(n> = MIN_MERGE){ r | =(n&1 ); n >> = 1 ; } 返回 n + r; }
2.求最小递增的长度,如果长度小于minRun,使用插入排序补充到minRun的个数,操作和小于32的个数是一样
.3。用栈记录每个run的长度,当下面的条件其中一个成立时归并,直到数量不变:
runLen [i - 3]> runLen [i - 2] + runLen [i - 1 ]
runLen [i - 2]> runLen [i - 1]
/ ** *检查等待合并的运行堆栈并合并相邻的运行 *直到重新建立堆栈不变量: * * 1. runLen [i - 3]> runLen [i - 2] + runLen [i - 1] * 2. runLen [i - 2]> runLen [i - 1] * *每次将新运行推入堆栈时都会调用此方法, *所以不变量保证为i*进入该方法。 * / private void mergeCollapse(){ while(stackSize> 1 ){ int n = stackSize - 2 ; if(n> 0 && runLen [n-1] <= runLen [n] + runLen [n + 1 ]){ if(runLen [n - 1] ]) n - ; mergeAt(N); } else if(runLen [n] <= runLen [n + 1 ]){ mergeAt(N); } else { break ; // 建立不变量 } } }
关于归并方法和对一般的归并排序做出了简单的优化。假设两个运行是run1,run2,先用gallopRight在run1里使用binarySearch查找run2首元素的位置k,那么run1中k前面的元素就是合并最后,根据len1与len2大小,调用mergeLo或者mergeHi将剩余元素合并。
/ ** *在堆栈索引i和i + 1处合并两次运行。我必须跑 *倒数第二个或倒数第二个在堆栈上运行。换一种说法, *我必须等于stackSize-2或stackSize-3。 * * @param 我将两个运行中的第一个的索引堆叠为合并 * / @SuppressWarnings( “unchecked” ) private void mergeAt(int i){ assert stackSize> = 2 ; 断言 i> = 0 ; 断言 i == stackSize - 2 || i == stackSize - 3 ; int base1 = runBase [i]; int len1 = runLen [i]; int base2 = runBase [i + 1 ]; int len2 = runLen [i + 1 ]; 断言 len1> 0 && len2> 0 ; 断言 base1 + len1 == base2; / * *记录组合运行的长度; 如果我是第三名 *现在运行,也可以在最后一次运行时滑动(不涉及 *在此合并中)。无论如何,当前的运行(i + 1)都会消失。 * / runLen [i] = len1 + len2; if(i == stackSize - 3 ){ runBase [i + 1] = runBase [i + 2 ]; runLen [i + 1] = runLen [i + 2 ]; } stackSize - ; / * *找到run2的第一个元素在run1中的位置。先前的要素 *在run1中可以忽略(因为它们已经到位)。 * / int k = gallopRight((Comparable ); 断言 k> = 0 ; base1 + = k; len1 - = k; if(len1 == 0 ) 返回; / * *找到run1的最后一个元素在run2中的位置。后续元素 *在run2中可以忽略(因为它们已经到位)。 * / len2 = gallopLeft((Comparable ],a, base2,len2,len2 - 1 ); 断言 len2> = 0 ; if(len2 == 0 ) 返回; // 合并剩余的运行,使用带有min(len1,len2)元素的tmp数组 if(len1 <= len2) mergeLo(base1,len1,base2,len2); 其他 mergeHi(base1,len1,base2,len2); }
4.最后归并还有没有归并的跑,知道跑的数量为1。
例子
为了演示方便,我将TimSort中的minRun直接设置为2,否则我不能用很小的数组演示。同时把MIN_MERGE也改成2(默认为32),这样避免直接进入二分插入排序。
1.初始数组为[7,5,1,2,6,8,10,12,4,3,9,11,13,15,16,14]
2.寻找第一个连续的降序或升序序列:[1,5,7] [2,6,8,10,12,4,3,9,11,13,15,16,14]
3. stackSize = 1,所以不合并,继续找第二个运行
4.找到一个递减序列,调整次序:[1,5,7] [2,6,8,10,12] [4,3,9,11,13,15,16,14]
5.因为runLen [0] <= runLen [1]所以归并
1)gallopRight:寻找run1的第一个元素应当插入run0中哪个位置(“2”应当插入“1”之后),然后就可以忽略之前run0的元素(都比run1的第一个元素小)
2)gallopLeft:寻找run0的最后一个元素应当插入run1中哪个位置(“7”应当插入“8”之前),然后就可以忽略之后run1的元素(都比run0的最后一个元素大)
这样需要排序的元素就仅剩下[5,7] [2,6],然后进行mergeLow完成之后的结果:[1,2,5,6,7,8,10,12] [4,3, 9,11,13,15,16,14]
6.寻找连续的降序或升序序列[1,2,5,6,7,8,10,12] [3,4] [9,11,13,15,16,14]
7.不进行归并排序,因为runLen [0]> runLen [1]
8.寻找连续的降序或升序序列:[1,2,5,6,7,8,10,12] [3,4] [9,11,13,15,16] [14]
9.因为runLen [1] <= runLen [2],所以需要归并
10.使用gallopRight,发现为正常顺序。得[1,2,5,6,7,8,10,12] [3,4,9,11,13,15,16] [14]
11.最后只剩下[14]这个元素:[1,2,5,6,7,8,10,12] [3,4,9,11,13,15,16] [14]
12.因为runLen [0] <= runLen [1] + runLen [2]所以合并。因为runLen [0]> runLen [2],所以将run1和run2先合并。(否则将run0和run1先合并)
完成之后的结果:[1,2,5,6,7,8,10,12] [3,4,9,11,13,14,15,16]
13.完成之后的结果:[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16]