timSort源码剖析

TimSort

timsort是jdk自带的一种特别高效的排序算法,大致思想使用的是归并排序,但是内部细节做了许多的优化.
在timsort中,主要是为待排序数组分为很多个run块,通过讲这些run块进行归并排序.最后实现总体排序.每个run块的大小为16-32大小.
优化地方:

  • 当待排序数组长度小于32就使用二分排序算法
  • 分为多个run块,在通过把run块的起始位置和长度压入栈中,在进行合并.
  • 在找到一个run块的时候会首先判断数组中有序元素的个数.通过二分排序从第一个无序的元素开始排序,加快排序速度
  • 在进行合并的时候会进行”去头”,”去尾”操作,是的归并操作加快速度.

sort(T[] a, int lo, int hi, Comparator<? super T> c, T[] work, int workBase, int workLen)

timsort对外的唯一接口.并且只能本包内访问

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
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;
//小于2不用排序,直接返回
if (nRemaining < 2)
return;
// 看是否小于min_merge,如果小于就直接二分排序,没必要进行复杂的timsort.
if (nRemaining < MIN_MERGE) {
//下面有详细说明.返回的是从lo开始已经有序的个数,对二分排序用
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);
//主要将数组分为一个个的minRun,最后在进行合并,如果长度为2的n次幂,minRun为32,否则为16-32之间的数.
int minRun = minRunLength(nRemaining);
do {
//再次找到a中已经有序的元素个数
int runLen = countRunAndMakeAscending(a, lo, hi, c);

//如果有序个数小于上面的最小minRun的话,就找到
if (runLen < minRun) {
int force = nRemaining <= minRun ? nRemaining : minRun;
binarySort(a, lo, lo + force, lo + runLen, c);
runLen = force;
}

// 将此run块放入run数组中.
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;
}

minRunLength

就是获得一个最小的Run块的大小,大小在16-32之间.

1
2
3
4
5
6
7
8
9
private static int minRunLength(int n) {
assert n >= 0;
int r = 0; // Becomes 1 if any 1 bits are shifted off
while (n >= MIN_MERGE) {
r |= (n & 1);
n >>= 1;
}
return n + r;
}

countRunAndMakeAscending(a, lo, hi, c)

找到a数组中从lo开始并且已经有序的元素个数,返回已经有序的元素个数.在这里如果逆序在翻转一下,变为有序.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private static <T> int countRunAndMakeAscending(T[] a, int lo, int hi,
Comparator<? super T> c) {
assert lo < hi;
int runHi = lo + 1;
if (runHi == hi)
return 1;

// 找见a从lo开始已经有序的元素个数
if (c.compare(a[runHi++], a[lo]) < 0) { // Descending
while (runHi < hi && c.compare(a[runHi], a[runHi - 1]) < 0)
runHi++;
reverseRange(a, lo, runHi);//如果是逆序则翻转
} else { // Ascending
while (runHi < hi && c.compare(a[runHi], a[runHi - 1]) >= 0)
runHi++;
}

return runHi - lo;
}

binarySort(T[] a, int lo, int hi, int start,Comparator<? super T> c)

此方法就是二分排序,二分排序就是插入排序,但是在寻找向前面插入的位置是通过二分法来查找,然后在插入的.
此方法是在a数组中排序从lo到hi的元素,从start开始.

private void mergeCollapse()

这个方法主要是用来合并已经分好的几个Run块的.但是是有条件的;

  1. runLen[i - 3] > runLen[i - 2] + runLen[i - 1]
  2. runLen[i - 2] > runLen[i - 1]
    满足这两个条件才合并,否则什么都不做.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    private void mergeCollapse() {
    //最少都得有两个块,如果没有,则退出.
    while (stackSize > 1) {
    int n = stackSize - 2;// stackSize为块的数量,-2相当与倒数第二个块
    //如果块的数量大于三个并且倒数第三个Run块大小小于后面两个块的大小和进入
    if (n > 0 && runLen[n-1] <= runLen[n] + runLen[n+1]) {
    //如果倒数第三个块比倒一块的大小小,就从倒数第三个块进行合并
    if (runLen[n - 1] < runLen[n + 1])
    n--;
    mergeAt(n);
    } else if (runLen[n] <= runLen[n + 1]) {//第一个块大小小于第二个块大小,就合并这两个
    mergeAt(n);
    } else {//否则什么都不做
    break; // Invariant is established
    }
    }
    }

private void mergeAt(int i)

再来看mergeAt(i)方法,这个方法主要就是合并在run块数组中从i开始的两个块.run[i]和run[i+1]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
int base1 = runBase[i];
int len1 = runLen[i];
int base2 = runBase[i + 1];
int len2 = runLen[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的最后一个位置,就不需要排序,因为run1中的所有元素都比run2中的小,直接返回,
//这样可以忽略掉run1中之前的位置
int k = gallopRight(a[base2], a, base1, len1, 0, c);
assert k >= 0;
base1 += k;
len1 -= k;
if (len1 == 0)
return;

//计算run1的最后一个元素在run2中插入的位置,进行去尾.
len2 = gallopLeft(a[base1 + len1 - 1], a, base2, len2, len2 - 1, c);
assert len2 >= 0;
if (len2 == 0)
return;

// 经过上面的“去头”和“去尾”之后,保证run1的开始元素一定大
// 于run2的开始元素,并且run1的最后一个数据一定大于run2的最后一个数据
// 然后进行合并,通过两个len的大小找到最好的合并方式
if (len1 <= len2)
mergeLo(base1, len1, base2, len2);
else
mergeHi(base1, len1, base2, len2);

private void mergeLo(int base1, int len1, int base2, int len2)

mergeLo方法 合并两个run块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
    T[] a = this.a; //首先赋值到一个临时数组中
T[] tmp = ensureCapacity(len1);
int cursor1 = tmpBase;
int cursor2 = base2; // Indexes int a
int dest = base1; // Indexes int a
//先吧a从base1开始的元素赋值到tmp从tmpbase开始的元素,赋值len1个元素,因为len1肯定是最小的元素
System.arraycopy(a, base1, tmp, cursor1, len1);

// 首先肯定run2的第一个元素小于run1的第一个元素,因为上面都已经去头了
a[dest++] = a[cursor2++];
if (--len2 == 0) {//如果run2长度为0,则赋值tmp过去,
System.arraycopy(tmp, cursor1, a, dest, len1);
return;
}
if (len1 == 1) {//如果run1只剩下一个元素,所以只需要先把run2复制过去,再讲Run1的唯一元素赋值过去
System.arraycopy(a, cursor2, a, dest, len2);
a[dest + len2] = tmp[cursor1]; // Last elt of run 1 to end of merge
return;
}

Comparator<? super T> c = this.c; // Use local variable for performance
int minGallop = this.minGallop; // 定义一个最小Gallop
outer: //定义跳出什么循环

//在这里有一个思路就是合并两个run,但是会记录两个块中连续个数的数量,如果连续个数的数量大于minGallop也就是7,那就会进入Gallop模式.此模式就是通过"去头","去尾"来减少比较次数
while (true) {
int count1 = 0; // Number of times in a row that first run won
int count2 = 0; // Number of times in a row that second run won

/*
* Do the straightforward thing until (if ever) one run starts
* winning consistently.
*/
do {
assert len1 > 1 && len2 > 0;
//开始比较
if (c.compare(a[cursor2], tmp[cursor1]) < 0) {
a[dest++] = a[cursor2++];
count2++;
count1 = 0;
if (--len2 == 0)
break outer;
} else {
a[dest++] = tmp[cursor1++];
count1++;
count2 = 0;
if (--len1 == 1)
break outer;
}
//当有一个块有连续大于另一个块的次数超过minGallop的时候,进入gallop模式
} while ((count1 | count2) < minGallop);

/*
* One run is winning so consistently that galloping may be a
* huge win. So try that, and continue galloping until (if ever)
* neither run appears to be winning consistently anymore.
*/
do {
assert len1 > 1 && len2 > 0;
//如上,去掉run1的头部
count1 = gallopRight(a[cursor2], tmp, cursor1, len1, 0, c);
if (count1 != 0) {
System.arraycopy(tmp, cursor1, a, dest, count1);
dest += count1;
cursor1 += count1;
len1 -= count1;
if (len1 <= 1) // len1 == 1 || len1 == 0
break outer;
}
a[dest++] = a[cursor2++];
if (--len2 == 0)
break outer;
// 去掉run2的尾部
count2 = gallopLeft(tmp[cursor1], a, cursor2, len2, 0, c);
if (count2 != 0) {
System.arraycopy(a, cursor2, a, dest, count2);
dest += count2;
cursor2 += count2;
len2 -= count2;
if (len2 == 0)
break outer;
}
a[dest++] = tmp[cursor1++];
if (--len1 == 1)
break outer;
minGallop--;
//在此模式每多循环一次minGallop减少1
} while (count1 >= MIN_GALLOP | count2 >= MIN_GALLOP);
if (minGallop < 0)
minGallop = 0;
//在离开这个gallop模式后值增加2
minGallop += 2; // Penalize for leaving gallop mode
} // End of "outer" loop

this.minGallop = minGallop < 1 ? 1 : minGallop; // Write back to field
if (len1 == 1) {
assert len2 > 0;
System.arraycopy(a, cursor2, a, dest, len2);
a[dest + len2] = tmp[cursor1]; // Last elt of run 1 to end of merge
} else if (len1 == 0) {
throw new IllegalArgumentException(
"Comparison method violates its general contract!");
} else {
assert len2 == 0;
assert len1 > 1;
//最后将tmp的值赋值到a中
System.arraycopy(tmp, cursor1, a, dest, len1);
}