一、项目背景详细介绍

在 Java 标准库中,Arrays.sort() 对对象数组使用的便是 TimSort 算法,它是一种高度优化的混合排序算法,结合了 归并排序(Merge Sort)与 插入排序(Insertion Sort)的优势,并利用了“天然有序区间”(run)的信息来加速排序。TimSort 最初由 Tim Peters 为 Python 实现,后被 Java、Android、Ruby 等多种语言采用。它在实际数据上通常能达到接近 O(n) 的性能,同时保证 O(n log n) 的最坏时间。

TimSort 的核心思想:

  1. 识别 runs

    • 在输入数组中扫描出已升序或降序的自然 run(最短长度通常≥32),将降序 run 反转为升序。

  2. 插入排序扩展短 run

    • 对短于 minRun 的 run 使用插入排序扩展到 minRun 长度。

  3. 合并 runs

    • 按照栈策略,将相邻 run 两两合并,利用“galloping”(跳跃)优化小块复制。

本文将系统地介绍 Java 中 TimSort 算法的实现思路与关键代码,并给出可运行的 TimSort 工具类。


二、项目需求详细介绍

  1. 功能需求

    • 提供 TimSort 类,静态方法:

      public static void sort(int[] a);

    • 对任意整数数组 a 原地排序,结果升序。

  2. 性能需求

    • 平均/最好:接近 O(n);

    • 最坏:O(n log n);

    • 额外空间 O(n) (辅助数组)。

  3. 健壮性需求

    • a == null 抛出 NullPointerException

    • 空数组或长度 < 2 直接返回。

  4. 可扩展性需求

    • 后续可泛化为 T[] 并支持 Comparator<T>

    • 可调整 MIN_MERGE 值以适配不同场景。


三、相关技术详细介绍

  1. 自然 run 识别

    • 一趟扫描,找出连续升序或降序片段;

    • 短 run 通过插入排序扩展到 minRun

  2. 最小 run 大小 minRun

    • 通常在 32–64 间:由数组长度决定,以保证合并树的高度较低。

  3. 归并栈策略

    • 将所有 run 压入栈,若栈顶三元素长度不满足 A > B + CB > C,就合并相邻较小 run。

  4. Gallisting(跳跃模式)

    • 在合并两个有序段时,当一侧接连胜出次数达到阈值,则切换到二分查找插入方式,减少单元素比较。


四、实现思路详细介绍

  1. 计算 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;
  }
}
  1. 归并两个 run

    • 复制较短 run 到临时数组;

    • 双指针合并,使用 galloping 模式提升小块复制速度。

  2. 最终合并

    • 处理完所有 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/LeftmergeLow/High 的具体跳跃合并实现,实际生产环境建议参考 OpenJDK 的完整实现(java.util.TimSort),以获得最佳性能与稳定性。


六、代码详细解读

  • minRunLength:根据数组长度计算最小 run 大小,兼顾合并效率与插入排序成本。

  • countRunAndMakeAscending:扫描并识别升序或降序 run,降序 run 需反转。

  • binarySort:对短 run 执行二分插入排序,扩展到 minRun 大小。

  • pushRunmergeCollapsemergeForceCollapse:维护 run 栈并根据长度关系触发合并。

  • mergeAt:合并栈顶两个 run,使用跳跃(galloping)策略优化大块复制。


七、项目详细总结

  1. TimSort 优势

    • 利用自然 run 降低时间;

    • 插入排序加速小块处理;

    • 跳跃合并减少比较次数。

  2. 复杂度

    • 平均/最好 O(n),最坏 O(n log n),空间 O(n)。

  3. 应用建议

    • 在大多数现实数据中表现优异;

    • 对半随机或部分有序数据更快。


八、项目常见问题及解答

  1. 为什么要反转降序 run?
    统一为升序,使后续合并逻辑简单。

  2. minRun 为什么不是常数?
    根据数据规模动态调整,以平衡分块与合并成本。

  3. 跳跃模式(galloping)是什么?
    当一侧连续取胜次数达到阈值时,以二分查找方式一次性拷贝一段,减少循环开销。


九、扩展方向与性能优化

  1. 完整 JDK 实现:参考 java.util.TimSort,包含 gallop* 与多线程优化。

  2. 泛型版本:实现 <T> 并支持 Comparator<T>

  3. 并行归并:基于 ForkJoinPool 分块并行合并。

  4. 增量排序:维护增量更新时的 run 栈,在线插入新元素并局部重排。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐