JAVA:实现TimSort算法(附带源码)
一、项目背景详细介绍
在 Java 标准库中,Arrays.sort() 对对象数组使用的便是 TimSort 算法,它是一种高度优化的混合排序算法,结合了 归并排序(Merge Sort)与 插入排序(Insertion Sort)的优势,并利用了“天然有序区间”(run)的信息来加速排序。TimSort 最初由 Tim Peters 为 Python 实现,后被 Java、Android、Ruby 等多种语言采用。它在实际数据上通常能达到接近 O(n) 的性能,同时保证 O(n log n) 的最坏时间。
TimSort 的核心思想:
-
识别 runs
-
在输入数组中扫描出已升序或降序的自然 run(最短长度通常≥32),将降序 run 反转为升序。
-
-
插入排序扩展短 run
-
对短于
minRun的 run 使用插入排序扩展到minRun长度。
-
-
合并 runs
-
按照栈策略,将相邻 run 两两合并,利用“galloping”(跳跃)优化小块复制。
-
本文将系统地介绍 Java 中 TimSort 算法的实现思路与关键代码,并给出可运行的 TimSort 工具类。
二、项目需求详细介绍
-
功能需求
-
提供
TimSort类,静态方法:public static void sort(int[] a); -
对任意整数数组
a原地排序,结果升序。
-
-
性能需求
-
平均/最好:接近 O(n);
-
最坏:O(n log n);
-
额外空间 O(n) (辅助数组)。
-
-
健壮性需求
-
对
a == null抛出NullPointerException; -
空数组或长度 < 2 直接返回。
-
-
可扩展性需求
-
后续可泛化为
T[]并支持Comparator<T>; -
可调整
MIN_MERGE值以适配不同场景。
-
三、相关技术详细介绍
-
自然 run 识别
-
一趟扫描,找出连续升序或降序片段;
-
短 run 通过插入排序扩展到
minRun。
-
-
最小 run 大小
minRun-
通常在 32–64 间:由数组长度决定,以保证合并树的高度较低。
-
-
归并栈策略
-
将所有 run 压入栈,若栈顶三元素长度不满足
A > B + C或B > C,就合并相邻较小 run。
-
-
Gallisting(跳跃模式)
-
在合并两个有序段时,当一侧接连胜出次数达到阈值,则切换到二分查找插入方式,减少单元素比较。
-
四、实现思路详细介绍
-
计算
minRun
private static int minRunLength(int n) {
int r = 0;
while (n >= MIN_MERGE) {
r |= (n & 1);
n >>= 1;
}
return n + r;
}
-
扫描并压栈 runs
-
扫描数组,识别升/降序 run,若降序则反转;
-
对短 run 使用
binarySort插入排序扩展到minRun; -
将 run(
base,len)压入栈,并调用mergeCollapse()。
-
-
合并策略
private void mergeCollapse() {
while (stackSize > 1) {
int n = stackSize - 2;
if (n > 0 && runLen[n-1] <= runLen[n] + runLen[n+1]
|| runLen[n] <= runLen[n+1]) {
if (runLen[n-1] < runLen[n+1]) n--;
mergeAt(n);
} else break;
}
}
-
归并两个 run
-
复制较短 run 到临时数组;
-
双指针合并,使用 galloping 模式提升小块复制速度。
-
-
最终合并
-
处理完所有 runs 后,迭代
mergeAt(0)合并直到栈只剩一个 run。
-
五、完整实现代码
// ===== 文件:TimSort.java =====
package com.example.sort;
import java.util.Arrays;
public class TimSort {
private static final int MIN_MERGE = 32;
private int[] a, tmp;
private int minGallop = 7;
private int stackSize = 0;
private final int[] runBase;
private final int[] runLen;
public TimSort(int[] array) {
this.a = array;
int n = array.length;
this.tmp = new int[n < 2 * MIN_MERGE ? n >>> 1 : MIN_MERGE];
int stackLen = (n < 120 ? 5 : n < 1542 ? 10 : n < 119151 ? 19 : 40);
runBase = new int[stackLen];
runLen = new int[stackLen];
}
public static void sort(int[] array) {
Objects.requireNonNull(array, "输入数组不能为空");
int n = array.length;
if (n < 2) return;
TimSort ts = new TimSort(array);
ts.sort(0, n);
}
private void sort(int lo, int hi) {
int n = hi - lo;
int minRun = minRunLength(n);
int i = lo;
while (i < hi) {
int runLen = countRunAndMakeAscending(i, hi);
if (runLen < minRun) {
int force = Math.min(minRun, hi - i);
binarySort(i, i + force, i + runLen);
runLen = force;
}
pushRun(i, runLen);
mergeCollapse();
i += runLen;
}
mergeForceCollapse();
}
/* ---------- 关键子方法(简版) ---------- */
private static int minRunLength(int n) {
int r = 0;
while (n >= MIN_MERGE) {
r |= (n & 1);
n >>= 1;
}
return n + r;
}
private int countRunAndMakeAscending(int lo, int hi) {
int runHi = lo + 1;
if (runHi == hi) return 1;
if (a[runHi++] < a[lo]) {
while (runHi < hi && a[runHi] < a[runHi - 1]) runHi++;
reverseRange(lo, runHi);
} else {
while (runHi < hi && a[runHi] >= a[runHi - 1]) runHi++;
}
return runHi - lo;
}
private void reverseRange(int lo, int hi) {
hi--;
while (lo < hi) {
int t = a[lo]; a[lo++] = a[hi]; a[hi--] = t;
}
}
private void binarySort(int lo, int hi, int start) {
for (int i = start; i < hi; i++) {
int pivot = a[i];
int left = lo, right = i;
while (left < right) {
int mid = (left + right) >>> 1;
if (pivot < a[mid]) right = mid;
else left = mid + 1;
}
System.arraycopy(a, left, a, left + 1, i - left);
a[left] = pivot;
}
}
private void pushRun(int base, int length) {
runBase[stackSize] = base;
runLen [stackSize] = length;
stackSize++;
}
private void mergeCollapse() {
while (stackSize > 1) {
int n = stackSize - 2;
if ((n > 0 && runLen[n-1] <= runLen[n] + runLen[n+1]) ||
(n > 0 ? runLen[n] <= runLen[n+1] : false)) {
if (runLen[n-1] < runLen[n+1]) n--;
mergeAt(n);
} else if (runLen[n] <= runLen[n+1]) {
mergeAt(n);
} else break;
}
}
private void mergeForceCollapse() {
while (stackSize > 1) {
int n = stackSize - 2;
mergeAt(n);
}
}
private void mergeAt(int i) {
int base1 = runBase[i], len1 = runLen[i];
int base2 = runBase[i+1], 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--;
int k = gallopRight(a[base2], a, base1, len1, 0);
base1 += k; len1 -= k;
if (len1 == 0) return;
len2 = gallopLeft(a[base1 + len1 - 1], a, base2, len2, len2 - 1);
if (len2 == 0) return;
if (len1 <= len2) mergeLow(base1, len1, base2, len2);
else mergeHigh(base1, len1, base2, len2);
}
/* gallopRight/Left, mergeLow/High 及跳跃合并略,详见 JDK 实现 */
}
// ===== 文件:Main.java =====
package com.example.sort;
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
int[] a = {5, 2, 9, 1, 5, 6, 3, 8, 7, 4};
System.out.println("排序前: " + Arrays.toString(a));
TimSort.sort(a);
System.out.println("TimSort 后: " + Arrays.toString(a));
}
}
注:以上代码为 简化版 TimSort 核心结构,省略了
gallopRight/Left与mergeLow/High的具体跳跃合并实现,实际生产环境建议参考 OpenJDK 的完整实现(java.util.TimSort),以获得最佳性能与稳定性。
六、代码详细解读
-
minRunLength:根据数组长度计算最小 run 大小,兼顾合并效率与插入排序成本。 -
countRunAndMakeAscending:扫描并识别升序或降序 run,降序 run 需反转。 -
binarySort:对短 run 执行二分插入排序,扩展到minRun大小。 -
pushRun、mergeCollapse、mergeForceCollapse:维护 run 栈并根据长度关系触发合并。 -
mergeAt:合并栈顶两个 run,使用跳跃(galloping)策略优化大块复制。
七、项目详细总结
-
TimSort 优势:
-
利用自然 run 降低时间;
-
插入排序加速小块处理;
-
跳跃合并减少比较次数。
-
-
复杂度:
-
平均/最好 O(n),最坏 O(n log n),空间 O(n)。
-
-
应用建议:
-
在大多数现实数据中表现优异;
-
对半随机或部分有序数据更快。
-
八、项目常见问题及解答
-
为什么要反转降序 run?
统一为升序,使后续合并逻辑简单。 -
minRun为什么不是常数?
根据数据规模动态调整,以平衡分块与合并成本。 -
跳跃模式(galloping)是什么?
当一侧连续取胜次数达到阈值时,以二分查找方式一次性拷贝一段,减少循环开销。
九、扩展方向与性能优化
-
完整 JDK 实现:参考
java.util.TimSort,包含gallop*与多线程优化。 -
泛型版本:实现
<T>并支持Comparator<T>。 -
并行归并:基于
ForkJoinPool分块并行合并。 -
增量排序:维护增量更新时的 run 栈,在线插入新元素并局部重排。
更多推荐
所有评论(0)