TimSort 是一种混合的、稳定的排序算法,结合了归并排序(Merge Sort)和二分插入排序(Binary Insertion Sort)的优点,尤其适用于部分有序的数据。在 Java 中,Arrays.sort() 对对象数组排序时内部使用了 TimSort 算法。

 对于集合的排序实际上也是使用Arrays.sort

如 List.java

    default void sort(Comparator<? super E> c) {Object[] a = this.toArray();Arrays.sort(a, (Comparator) c);ListIterator<E> i = this.listIterator();for (Object e : a) {i.next();i.set((E) e);}}

类的作用​

  • 主要用于对数组进行高效且稳定的排序。
  • Java 的 Arrays.sort() 方法在排序对象数组时调用 TimSort。

核心思想​

  1. ​识别自然有序子序列(run)​​:
    遍历数组,找到已有序的连续子序列。
  2. ​扩展短 run​​:
    若 run 长度小于 MIN_MERGE,使用二分插入排序将其扩展到最小长度。
  3. ​合并 run​​:
    通过归并操作将所有 run 合并为完全有序的数组。

TimSort 如何实现非递归的归并排序?

传统的归并排序通常使用递归,将数组不断对半分割,直到子数组只有一个元素,然后逐层返回并合并。这个过程依赖于程序的调用栈(call stack)来管理子问题。

TimSort 的巧妙之处在于它​​用一个自己管理的显式栈(runBaserunLen 数组)替代了递归所需的调用栈​​。整个过程如下:

  1. ​遍历与压栈​​:一个简单的循环从左到右遍历数组,找到或创建小的有序片段(run)。
  2. ​管理子问题​​:每找到一个 run,就调用 pushRun 将其信息压入自己的栈中。
  3. ​智能合并​​:pushRun 之后立即调用 mergeCollapse,根据预设的平衡策略,决定是否要合并栈顶的某些 run。这个合并决策是迭代进行的,而不是递归返回时才发生。

通过这种方式,TimSort 将递归的“分治”思想转换成了一个迭代的过程,避免了递归深度过大可能导致的 StackOverflowError,并且通过 mergeCollapse 的智能合并策略,进一步优化了归并的效率。这就是它实现非递归归并排序的原理。

有状态设计

Arrays.sort没有创建新实例,而是内部递归进行归并排序的时候创建实例。

有状态是因为归并排序需要复制。

这个私有的 TimSort 构造函数主要做了以下几件重要的事情,本质上都是为了初始化排序过程需要的数据结构和状态:

  1. 保存核心排序参数 :

    1. this.a = a; :保存待排序的数组的引用。

    2. this.c = c; :保存用于比较元素顺序的比较器 Comparator 。

  2. 分配临时存储空间 ( tmp 数组) :

    1. TimSort 算法的核心是归并,在归并两个已排序的子序列(称为 "run")时,需要一个临时的存储空间来存放其中一个子序列。构造函数会预先分配这个名为 tmp 的数组。

    2. 为了优化性能和内存使用,它会计算一个合适的初始大小。如果调用者(例如 Arrays.sort )提供了一个足够大的工作区数组 ( work ),它会直接使用,避免重复创建数组。

  3. 分配用于管理 "run" 的栈 ( runBase 和 runLen 数组) :

    1. TimSort 算法会识别出数组中已经排好序的片段(称为 "run"),然后将这些 "run" 合并。它使用一个栈来管理这些待合并的 "run"。

    2. runBase 数组存储每个 "run" 的起始索引。

    3. runLen 数组存储每个 "run" 的长度。

    4. 构造函数会根据待排序数组的长度 len 计算出一个足够大但又不过于浪费的栈深度 stackLen ,然后创建这两个数组。

总而言之, TimSort 的构造函数是一个 初始化和准备 的过程,它建立了一个包含所有排序所需上下文(待排序数组、比较器、临时空间、管理栈)的“工作台”,使得后续的排序步骤可以高效地进行。

关键属性​

属性名说明
MIN_MERGE (32)最小 run 长度。短 run 会被扩展至此值,以平衡插入排序和归并排序的效率。
MIN_GALLOP (7)控制“galloping mode”的阈值,减少连续比较次数。
INITIAL_TMP_STORAGE_LENGTH (256)临时存储数组的初始大小,用于归并操作。
a待排序的数组。
c比较器(若为 null,使用自然顺序)。
tmp临时数组,用于归并操作。
runBaserunLen存储 run 的起始位置和长度。stackSize 记录当前栈中 run 的数量。

关键方法​

​构造函数​

  • TimSort(T[] a, Comparator<? super T> c, T[] work, int workBase, int workLen)
    • 初始化实例,设置数组和比较器。
    • 根据数组长度分配临时数组 tmp 和 run 信息数组(runBaserunLen)。

​核心方法​

  • sort(T[] a, int lo, int hi, Comparator<? super T> c, ...)

    • ​主入口点​​:由 Arrays.sort() 调用。
    • ​小数组处理​​:若长度小于 MIN_MERGE,直接使用二分插入排序。
    • ​主循环​​:
      1. 调用 countRunAndMakeAscending 识别自然 run。
      2. 若 run 过短,用 binarySort 扩展。
      3. 压入 run 栈(pushRun)并合并(mergeCollapse)。
    • ​最终合并​​:循环结束后调用 mergeForceCollapse 完成排序。
  • countRunAndMakeAscending

    • 返回自然 run 的长度,并确保其为升序(降序则反转)。
  • binarySort

    • 对小规模数据执行稳定的二分插入排序。
  • minRunLength

    • 计算最小 run 长度(介于 MIN_MERGE/2MIN_MERGE 之间)。
  • mergeCollapsemergeForceCollapse

    • ​合并规则​​:保持栈中 run 长度满足 runLen[i-2] > runLen[i-1] + runLen[i]
    • 强制合并剩余 run 直到完全有序。
  • mergeLomergeHi

    • 实际归并操作,根据 run 大小选择合并方向(低索引或高索引优先)。

与 ComparableTimSort 的关系​

  • ComparableTimSort​ 是 TimSort 的变体,专为实现了 Comparable 的对象数组设计,直接调用 compareTo 方法,无需显式比较器。

总结​

TimSort 通过以下策略实现高效排序:

  1. ​动态适应数据特性​​:识别自然 run 并智能选择排序策略。
  2. ​平衡合并操作​​:通过栈规则避免低效合并。
  3. ​混合算法优势​​:结合二分插入排序(小数据)和归并排序(大数据)的优点。

其设计使其在各类场景下均表现优异,成为 Java 默认排序算法之一。

sort

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

这个静态方法是 TimSort 算法的入口。核心思想:

  1. 找出数组中已存在的有序片段(称为 "run")。
  2. 高效合并这些 run。

第一步:处理小数组(Mini-TimSort)

​条件​​:待排序元素数量 nRemaining < MIN_MERGE(默认 32)

  1. countRunAndMakeAscending(a, lo, hi, c)

    • 从数组开头找到第一个自然有序的 run(升序或降序)。
    • 若为降序,通过 reverseRange 反转为升序。
    • 返回 run 的长度 initRunLen
  2. binarySort(a, lo, hi, lo + initRunLen, c)

    • 对剩余元素(lo + initRunLenhi)使用二分插入排序。
    • 优势:对小规模且部分有序的数据效率极高。

二分插入排序,二分找到后,直接利用array copy

binary Sort/** The invariants still hold: pivot >= all in [lo, left) and* pivot < all in [left, start), so pivot belongs at left.  Note* that if there are elements equal to pivot, left points to the* first slot after them -- that's why this sort is stable.* Slide elements over to make room for pivot.*/int n = start - left;  // The number of elements to move// Switch is just an optimization for arraycopy in default caseswitch (n) {case 2:  a[left + 2] = a[left + 1];case 1:  a[left + 1] = a[left];break;default: System.arraycopy(a, left, a, left + 1, n);}a[left] = pivot;


第二步:处理大数组(完整 TimSort)

​条件​​:数组长度 ≥ MIN_MERGE

​初始化​

  • new TimSort<>(...):创建实例,初始化临时数组 tmp 和 run 栈(runBase/runLen)。
        TimSort<T> ts = new TimSort<>(a, c, work, workBase, workLen);

​计算最小 run 长度​

  • minRunLength(nRemaining):确保 nRemaining / minRun 接近或略小于 2 的幂,使归并操作均衡。

Timsort 是一种混合排序算法,它通过合并一系列已经排好序的子数组(称为 "run")来完成整个数组的排序。为了让合并过程最高效,理想的情况是每次合并的两个 run 的长度都差不多。 minRunLength 方法的目的就是计算出一个合适的最小 run 长度( minRun ),使得原数组可以被分割成数量接近 2的幂 的 run。这样在后续的归并操作中,可以持续进行大小相近的合并,从而达到最优性能。

int minRun = minRunLength(nRemaining);private static int minRunLength(int n) {assert n >= 0;int r = 0;      // Becomes 1 if any 1 bits are shifted offwhile (n >= MIN_MERGE) {r |= (n & 1);n >>= 1;}return n + r;}

如果 n 不是 k * (2^m) (其中 k 是最终的 n 值,MIN_MERGE=2^5) 这种“干净”的数, r 就会是 1。

  1. 最终返回的是循环结束后的 n 加上标志位 r 。这相当于:如果原始的 n 不能被最终的 n 整除(即存在“余数”,导致 r 为1),那么就把 minRun 的长度加 1。这样做可以减少 run 的总数,使其更接近 2 的幂。

用一个例子 n = 65 来看看:

  1. 初始时 n = 65 , r = 0 。

  2. 65 >= 32 ,进入循环。

    1. n 是 65 (奇数), n & 1 是 1。 r |= 1 ,所以 r 变为 1。

    2. n >>= 1 , n 变为 32。

  3. 32 >= 32 ,继续循环。

    1. n 是 32 (偶数), n & 1 是 0。 r |= 0 , r 仍然是 1。

    2. n >>= 1 , n 变为 16。

  4. 16 < 32 ,循环结束。

  5. 返回 n + r ,即 16 + 1 = 17 。

所以,对于一个长度为 65 的数组,Timsort 会确保每个 run 的长度至少为 17。这样 65 / 17 ≈ 3.82 ,数组会被分成 4 个 run。4 是 2 的幂,非常适合归并。

如果没有 r , minRun 就是 16。 65 / 16 = 4 余 1。这会产生 4 个长度为 16 的 run 和 1 个长度为 1 的 run。合并一个长度为 16 和一个长度为 1 的 run 效率就不那么高了。

数学原理说明

注意n < 32是小数组处理的,对于大数组处理minRun的输入一定是n>=32。

在Java的 TimSort 实现中, MIN_MERGE 的值是32。

这个循环 相当于把n分为高5位 q,和剩余位 b,如果b不是0,则会+1

循环结束后 n 的值 q 的范围是 [16, 31](高5位就一定是这个范围,如果n>=32)

当我们把除数从 q 变为 q+1 时,商和余数都会改变。但这个算法的巧妙之处在于,它的目标 ​​不是​​ 去管理余数的大小,而是 ​​确保最终run的总数​​(即 ceil(N / minrun) )非常接近一个2的幂 。

我们来做一个更严谨的分析:

设数组总长为 N 。设循环右移了 s 次。
我们得到 q = N >> s (即 q = floor(N / 2^s) )和 r (0或1)。 minrun = q + r 。
我们要分析的run数量是 k = ceil(N / minrun) 。

  • ​情况A​​: r = 0
    此时 N 的低 s 位全是0, N = q * 2^s 。 minrun = q 。 k = ceil((q * 2^s) / q) = 2^s 。
    此时run的数量不多不少,正好是一个2的幂,这是最理想的情况。

  • ​情况B​​: r = 1
    此时 N 的低 s 位不全为0, N = q * 2^s + rem ,其中 0 < rem < 2^s 。 minrun = q + 1 。
    run的数量 k = ceil( (q * 2^s + rem) / (q + 1) ) 。

    我们来为这个表达式找一个上下界:

    • ​上界​​:
      N = q * 2^s + rem < q * 2^s + 2^s = (q+1) * 2^s 。
      所以 N / minrun = N / (q+1) < ((q+1) * 2^s) / (q+1) = 2^s 。
      因为 N / (q+1) 严格小于 2^s ,所以 k = ceil(N / (q+1)) 最多是 2^s 。

    • ​下界​​:
      N = q * 2^s + rem > q * 2^s 。
      所以 N / minrun = N / (q+1) > (q * 2^s) / (q+1) 。
      因此 k = ceil(N / (q+1)) > (q * 2^s) / (q+1) \approx (1 - 1/(q+1)) * 2^s 。

    结合 q 的范围是 [16, 31] 。

    • 当 q 取最小值16时, q/(q+1) = 16/17 ≈ 0.941 。
    • 当 q 取最大值31时, q/(q+1) = 31/32 ≈ 0.969 。
      这意味着 k 的范围被严格限制在 ceil(0.941 * 2^s) 和 2^s 之间。

    ​举例​​:

    • 假设 s=5 ,那么目标run数是 2^5 = 32 。 k 的下界是 ceil(0.941 * 32) = ceil(30.112) = 31 。
      所以,当目标run数是32时,实际的run数 k 只可能是31或32。
    • 假设 s=6 ,目标run数是 2^6 = 64 。 k 的下界是 ceil(0.941 * 64) = ceil(60.224) = 61 。
      所以,当目标run数是64时,实际的run数 k 被限制在 [61, 64] 这个极小的范围内。

该算法通过将 q 限制在 [MIN_MERGE/2, MIN_MERGE-1] 范围内,并根据余数是否存在来决定 minrun 是 q 还是 q+1 ,最终确保了run的总数 k 要么恰好是一个2的幂 2^s ,要么是在一个非常贴近 2^s 的极小区间内。这为后续归并操作的平衡性提供了强有力的保证,是Timsort高性能的关键之一。

​主循环(do-while)​

  • ​a. countRunAndMakeAscending(...)
    同 Mini-TimSort,找到下一个自然 run。
  • ​b. 扩展短 run​
    若当前 runLen < minRun,通过 binarySort 强制扩展到 minRun 长度(或剩余元素总数)。
  • ​c. ts.pushRun(lo, runLen)
    将 run 的起始位置和长度压入栈。
  • ​d. ts.mergeCollapse()
    检查栈顶 run 是否满足“栈不变式”(如 runLen[i-2] > runLen[i-1] + runLen[i])。若不满足,调用 mergeAt 合并相邻 run。
  • ​e. 更新索引​
    移动 lonRemaining,准备处理下一个 run。
  1. ​最终合并​

    • ts.mergeForceCollapse():合并栈中剩余 run,直到只剩一个 run,完成排序。

子函数总体说明

mergeCollapse() -> mergeAt(n)

  • ​职责​​:维持栈的平衡。

  • ​操作​​:检查栈顶 run 长度,若不平衡则计算最佳合并点 n,调用 mergeAt(n)

mergeAt(i) -> gallopRight(), gallopLeft(), mergeLo(), mergeHi()

  1. ​优化 1:跳过有序部分​

    • gallopRight:找到 run2 的首元素在 run1 中的插入点,跳过 run1 中已有序部分。

    • gallopLeft:找到 run1 的末元素在 run2 中的插入点,跳过 run2 末尾有序部分。

  2. ​优化 2:选择合并策略​

    • 根据剩余长度选择 mergeLorun1 较短)或 mergeHirun2 较短),最小化临时数组使用。

mergeLo() / mergeHi() -> gallopRight(), gallopLeft()

  • ​实际归并操作​​:逐个比较元素并归并。

  • ​优化 3:Galloping 模式​

    • 若一个 run 的元素连续多次“胜出”,进入飞奔模式,调用 gallopRight/gallopLeft 批量移动数据块。

    • minGallop 动态调整进入/退出此模式的阈值。

gallopLeft() / gallopRight()

  • ​飞奔搜索​​:
    1. 指数级步长(1, 3, 7, 15...)快速定位范围。
    2. 在小范围内执行二分查找,高效定位插入点。

pushRun(int runBase, int runLen)

这个方法非常直接,它的作用是将一个已经排好序的连续片段(run)的信息记录下来,存入一个专门的“待合并区”——也就是代码中的 runBase 和 runLen 数组,它们共同构成了一个栈。

  • runBase : 记录这个 run 在原数组中的起始索引。

  • runLen : 记录这个 run 的长度。

  • stackSize : 记录当前栈中有多少个待合并的 run。

TimSort 的主循环会遍历整个数组,识别或创建这些小的有序片段(run),然后调用 pushRun 把它们一个个推到这个栈上,等待后续的合并操作。

mergeCollapse()

这是 TimSort 算法的精髓所在。每当一个新的 run 被 pushRun 推入栈顶后,mergeCollapse 就会被调用。它的任务是检查栈顶的几个 run 是否满足特定的“平衡”条件(即注释中提到的两个不变式)。

  • ​不变式 1​​: runLen[i - 2] > runLen[i - 1]
  • ​不变式 2​​: runLen[i - 3] > runLen[i - 2] + runLen[i - 1]

这些不变式的核心目标是​​保持栈上 run 的长度大致平衡​​,避免出现一个非常长的 run 和一个非常短的 run 进行合并,因为那样效率不高。

mergeCollapse 会持续检查栈顶的 run,如果不满足这些条件,它就会选择相邻的两个 run 调用 mergeAt(n) 方法进行合并,直到栈恢复平衡状态。通过这种方式,它能确保合并操作总是在大小相近的 run 之间进行,从而最大化效率。

    private void mergeCollapse() {while (stackSize > 1) {int n = stackSize - 2;if (n > 0 && runLen[n-1] <= runLen[n] + runLen[n+1] ||n > 1 && runLen[n-2] <= runLen[n] + runLen[n-1]) {if (runLen[n - 1] < runLen[n + 1])n--;} else if (n < 0 || runLen[n] > runLen[n + 1]) {break; // Invariant is established}mergeAt(n);}}

简单来说,该方法遵循两条规则(不变量),并持续检查栈顶的几个 run 是否满足这些规则。如果不满足,就进行合并;如果满足,就暂时跳过,等待新的 run 加入。

让我们把栈顶的三个 run (从栈底到栈顶方向)想象成 X, Y, Z。这两条规则是:

  1. len(X) > len(Y) + len(Z)

  2. len(Y) > len(Z)

当栈上 run 的长度违反了上述任何一条规则时,就需要进行合并。代码中的 if 语句正是用于检查这些违规情况:

if (n > 0 && runLen[n-1] <= runLen[n] + runLen [n+1] ||   n > 1 && runLen[n-2] <= runLen[n] + runLen[n-1]) {
  • runLen[n-1] <= runLen[n] + runLen[n+1] 检查的是规则 1 ( len(X) > len(Y) + len(Z) ) 是否被违反。

  • runLen[n-2] <= runLen[n] + runLen[n-1] 检查的是更深一层(W, X, Y)的 run 是否违反了规则 1。

  • 如果以上两个条件都不成立,代码会进入 else if 分支。如果此时 runLen[n] <= runLen[n+1] ,则说明规则 2 ( len(Y) > len(Z) ) 被违反,同样需要合并。

一旦决定合并,算法会优先合并两个长度较小的相邻 run ,以维持整体的平衡。这就是 if (runLen[n - 1] < runLen[n + 1]) 这行代码的作用:

  • 如果 len(X) < len(Z) ,就合并 X 和 Y。

  • 否则,合并 Y 和 Z。

这个合并过程会一直循环,直到栈上所有的 run 都满足那两条不变量为止。

什么时候可以跳过(break)?

当栈顶的 run 已经满足了不变量时,就不需要再进行合并了,可以跳出循环。 else if 中的这个条件负责判断:

} else if (n < 0 || runLen[n] > runLen[n + 1])  {     break; // Invariant is established }

这里的 runLen[n] > runLen[n + 1] 正是在检查规则 2 ( len(Y) > len(Z) )。如果这个条件成立,并且前面更复杂的规则 1 检查也通过了,就意味着栈目前是“稳定”的,可以暂时停止合并,继续去数组中寻找下一个 run 。

mergeForceCollapse

循环结束后,最终强制合并,同样的优化是 先合并小的

    private void mergeForceCollapse() {while (stackSize > 1) {int n = stackSize - 2;if (n > 0 && runLen[n - 1] < runLen[n + 1])n--;mergeAt(n);}}

mergeAt 

mergeAt 函数之所以实现复杂,是因为它并非简单的归并操作,而是 TimSort 这一高效、稳定排序算法的核心优化所在。其复杂性旨在为真实世界中常见的部分有序数据提供极致性能。

TimSort 首先将输入数组分解为多个已排序的子序列(称为 "run")。mergeAt 的任务是将栈上相邻的两个 run(例如 run[i]run[i+1])合并为一个更大的有序 run。

简单归并排序会逐个比较元素,而 TimSort 通过智能策略避免对部分有序数据的无效操作。

代码逐段解析

  1. ​准备与栈管理​

    private void mergeAt(int i) {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--;
    • 获取两个 run 的起始位置和长度。
    • 更新栈信息,合并 run 并减少栈大小。
  2. ​第一次优化:gallopRight

    int k = gallopRight(a[base2], a, base1, len1, 0, c);
    assert k >= 0;
    base1 += k;
    len1 -= k;
    if (len1 == 0) return;
    • 取出 run2 的第一个元素,在 run1 中快速查找其插入位置。
    • 通过指数级步长(1, 3, 7, 15...)跳过 run1 中所有小于该元素的区间。
    • 跳过部分无需参与后续合并,减少比较次数。
  3. ​第二次优化:gallopLeft

    len2 = gallopLeft(a[base1 + len1 - 1], a, base2, len2, len2 - 1, c);
    assert len2 >= 0;
    if (len2 == 0) return;
    • 取出 run1 的最后一个元素,在 run2 中反向查找插入位置。
    • 跳过 run2 中所有大于该元素的区间。
    • 精确缩小需合并的范围。
  4. ​第三次优化:选择 mergeLomergeHi

    if (len1 <= len2)mergeLo(base1, len1, base2, len2);
    elsemergeHi(base1, len1, base2, len2);
    • 根据剩余长度选择合并策略:
      • mergeLo:当 len1 <= len2 时,复制较短的 run1 到临时空间。
      • mergeHi:当 len1 > len2 时,复制较短的 run2 到临时空间。
    • 确保临时空间不超过 N/2,最小化数据拷贝。

gallopLeft 函数的复杂性

结合两种搜索策略:

  1. ​指数搜索(Galloping)​​:从 hint 位置以 2^k - 1 步长跳跃,快速定位范围。
  2. ​二分搜索​​:在指数搜索确定的范围内精确查找插入点。
    这种“先粗后精”的方式对结构化数据效率远超纯二分搜索。

总结

mergeAt 的复杂性体现了 TimSort 的精髓:

  • ​适应性​​:通过 gallop 模式高效处理已有顺序的数据。
  • ​效率​​:减少无效比较和数据移动,优化内存分配。

正是这些设计使 TimSort 成为 Java、Python 等语言标准库的默认排序算法。

gallopLeft 

gallopLeft 的核心目标是:
在一个已排序的数组(或数组的一部分)中,快速地为一个给定的 key 找到它应该插入的位置。
如果数组中存在与 key 相等的元素,它会返回 ​​最左侧​​ 那个相等元素对应的索引。
这个特性对于保持排序的稳定性至关重要。

函数签名

private static <T> int gallopLeft(T key, T[] a, int base, int len, int hint, Comparator<? super T> c
)

参数说明

  • key:要在数组 a 中查找插入点的元素。
  • a:进行查找的目标数组。
  • base:查找范围在数组 a 中的起始索引。
  • len:查找范围的长度。
  • hint:一个“提示”索引,表示 key 可能的位置。
    这是 TimSort 适应性的关键,它假设数据具有局部性,即下一个要插入的元素很可能在前一个元素附近。

gallopLeft 的执行过程分为两个主要阶段:
​“飞驰模式”(Galloping)​​ 和 ​​二分查找(Binary Search)​​。


阶段一:飞驰模式(指数式搜索)

此阶段的目标是利用 hint 快速定位一个包含 key 的较小范围,而不是从头开始进行二分查找。

  1. ​方向判断​
    首先,比较 keya[base + hint] 的值:

    • 如果 c.compare(key, a[base + hint]) > 0,说明 keyhint 的右侧。此时,算法会向右“飞驰”。
    • 如果 key <= a[base + hint],说明 keyhint 的左侧或就是 hint 位置的元素。此时,算法向左“飞驰”。
  2. ​指数级步进​
    算法以指数级增加的步长(1, 3, 7, 15, ...,偏移量由 ofs = (ofs << 1) + 1 计算)进行探测,直到找到一个区间 [lastOfs, ofs],使得 key 恰好落在这个区间内。
    例如,向右飞驰时,直到满足:
    a[base + hint + lastOfs] < key <= a[base + hint + ofs]
    这种方式使得当 key 的实际位置距离 hint 很远时,也能极快地缩小查找范围。


阶段二:二分查找

  1. ​范围确定​
    飞驰阶段结束后,已经确定了一个比原始范围 len 小得多的精确范围 [lastOfs, ofs]

  2. ​经典二分查找​
    在此小范围内,执行一次标准的二分查找来精确定位插入点:

    • 循环条件为 while (lastOfs < ofs)
    • 在查找过程中:
      • 如果 c.compare(key, a[base + m]) > 0,意味着 key 在中间点 m 的右边,因此将搜索范围的左边界更新为 m + 1
      • 如果 key <= a[base + m],意味着 keym 的左边,或者 a[base + m] 就是一个与 key 相等的元素。
        为了找到 ​​最左侧​​ 的插入点,算法会继续在左半部分查找(ofs = m),而不是立即返回。
        这确保了即使找到一个匹配项,也会继续向左探索是否还有更早的匹配项。
  3. ​返回结果​
    最终,lastOfsofs 会重合,这个重合点就是 key 的最左插入位置。函数返回该偏移量 ofs


gallopLeft 是 TimSort 算法能够适应不同数据分布并保持高效的关键所在。
它通过 ​​“指数搜索 + 二分查找”​​ 的两阶段策略,避免了在数据高度有序或存在大段连续区块时进行逐一比较的低效操作。
通过与 mergeLomergeHi 的协同工作,它实现了智能的“飞驰模式”,使得 TimSort 在处理真实世界中常见的、部分有序的数据时,性能远超传统的归并排序。

TimSort.mergeLo

TimSort 是一种混合稳定排序算法,结合了归并排序和插入排序的优点,被应用于 Java 的 Arrays.sort(Object[]) 以及 Python 的 list.sort()sorted() 中。mergeLo 方法是其归并操作的核心实现之一。

mergeLo 的主要任务是 ​​原地、稳定地​​ 合并两个已经排好序且相邻的子数组(在 TimSort 中称为 "run")。

  • ​Lo 的含义​​:
    这个方法被设计用于 run1 的长度(len1)小于或等于 run2 的长度(len2)的场景。这样做是为了优化内存使用,因为它总是将 ​​较短​​ 的 run 复制到临时空间中,从而最小化额外空间开销。

  • ​稳定性​​:
    在合并过程中,如果遇到相等的元素,mergeLo 会优先保留原先排在前面的元素(来自 run1),从而保证了排序的稳定性。


让我们一步步解析代码的执行流程:

1) 初始化与数据准备

// ...
T[] a = this.a;          // 减少字段访问,提升性能
T[] tmp = ensureCapacity(len1); // 确保临时数组 tmp 有足够容量
System.arraycopy(a, base1, tmp, cursor1, len1); // 将第一个 run(较短的)完整复制到 tmp 中
// ...

这是 mergeLo 策略的核心:只复制 run1。现在,原数组 a[base1, base1 + len1) 这段空间就可以作为合并后的目标区域了。

2) 处理特殊情况(Degenerate Cases)

// ...
a[dest++] = a[cursor2++]; // 移动 run2 的第一个元素
if (--len2 == 0) { /* ... */ } // 如果 run2 只有一个元素
if (len1 == 1) { /* ... */ }   // 如果 run1 只有一个元素
// ...

代码首先无条件地将 run2 的第一个元素移动到目标位置。这是一个优化,因为 run2 的第一个元素通常小于 run1 的最后一个元素,可以直接放置。随后,代码快速处理了其中一个 run 长度极短(为 1 或 0)的边界情况,避免进入复杂的主循环。

3) 主合并循环:常规合并与"飞驰模式"的切换

这是函数最精妙的部分。它在一个 while(true) 循环中,根据数据的局部有序性,在两种模式间自适应切换。

  • ​常规合并阶段​​:
do {// ...if (c.compare(a[cursor2], tmp[cursor1]) < 0) {a[dest++] = a[cursor2++];count2++; count1 = 0;} else {a[dest++] = tmp[cursor1++];count1++; count2 = 0;}
} while ((count1 | count2) < minGallop);
  • 这个 do-while 循环执行的是标准的"一次比较,一次移动"的归并操作。

  • count1count2 记录了每个 run 连续获胜(即其元素被选中)的次数。

  • 当任何一个 run 连续获胜的次数达到 minGallop 阈值时,循环退出,算法认为数据出现了高度的局部有序性,适合切换到更高效的模式。

  • ​飞驰模式 (Galloping Mode)​​:

do {// ...count1 = gallopRight(a[cursor2], tmp, cursor1, len1, 0, c);// ... 批量复制 ...count2 = gallopLeft(tmp[cursor1], a, cursor2, len2, 0, c);// ... 批量复制 ...
} while (count1 >= MIN_GALLOP | count2 >= MIN_GALLOP);
  • ​目的​​:
    当一个 run 的元素持续小于另一个 run 时,逐个比较就显得低效。飞驰模式通过一种类似二分查找的方式(gallopLeft / gallopRight),快速跳过另一个 run 中一长段连续的元素。

  • ​过程​​:
    例如,gallopRight(a[cursor2], tmp, ...) 会在 tmprun1)中快速查找有多少个元素小于 a[cursor2]。然后通过 System.arraycopy 将这些元素进行 ​​批量复制​​,极大地提升了效率。

  • ​模式切换​​:
    这种模式会一直持续,直到两个 run 的批量复制长度(count1count2)都小于 MIN_GALLOP,表明数据的有序性不再明显,此时会退回到常规合并模式。

4) 收尾工作

// ...
if (len1 == 1) { /* ... */ }       // run1 还剩一个元素
else if (len1 == 0) { throw new IllegalArgumentException(...); }
else { System.arraycopy(tmp, cursor1, a, dest, len1); } // run2 已耗尽,复制 run1 剩余部分

循环结束后,必然有一个 run 已经被完全合并。这部分代码负责将另一个 run 中剩余的所有元素复制到目标数组的末尾。


算法精髓——自适应性

TimSort 的强大之处在于其自适应性,这在 mergeLo 中通过 minGallop 变量体现得淋漓尽致:

  • ​进入飞驰模式​​:
    当数据有序性高时(一个 run 连续获胜),count 迅速达到 minGallop,进入飞驰模式以加速处理。

  • ​惩罚与奖励​​:

    • minGallop--:在飞驰模式中,每次成功的 gallop 都会让 minGallop 减 1,使得下一次更容易保持在飞驰模式。
    • minGallop += 2:如果飞驰模式效果不佳并退出,minGallop 会加 2,使得下次进入飞驰模式的门槛变高,避免在随机性强的数据上浪费时间。

这种机制使得 TimSort 能够动态适应输入数据,无论数据是接近有序还是完全随机,都能提供接近最优的性能。


    总结

    TimSort 是高度优化的稳定混合排序算法,融合以下技术:

    1. ​基本框架​​:归并排序。
    2. ​小数组/短 run 处理​​:二分插入排序(binarySort)。
    3. ​归并策略​​:通过栈不变式(mergeCollapse)实现智能合并。
    4. ​性能加速​​:飞奔模式(gallopLeft/gallopRight)适应部分有序数据。

    ​优势​​:对真实世界数据(高度有序或完全随机)均表现卓越。

    TimSort 和 DualPivotQuicksort 对比

    TimSort 和 DualPivotQuicksort 是两种不同的排序算法,应用在不同的场景下。除了分别用于对象和基本类型数组外,它们的主要差别在于算法核心、稳定性、性能和空间复杂度。

    实际上DualPivotQuicksort实现有更多技巧,见:

    深入浅出 Arrays.sort(DualPivotQuicksort):如何结合快排、归并、堆排序和插入排序


    核心算法逻辑

    ​DualPivotQuicksort (双轴快速排序):​

    • 是对经典快速排序算法的改进:
      • 传统快速排序选择一个“轴点”(pivot),将数组分为两部分(小于轴点的和大于轴点的)。
      • 双轴快速排序选择两个轴点,将数组分为三部分:小于第一个轴点的、在两个轴点之间的、大于第二个轴点的,然后递归排序。
    • 优势:
      • 比单轴快速排序性能更好,能更好地处理数据分布,减少递归深度。
      • 对于非常小的数组,会切换到插入排序(Insertion Sort)以提高效率。

    ​TimSort:​

    • 是一种混合(Hybrid)排序算法,结合了归并排序(Merge Sort)和插入排序(Insertion Sort)的优点:
      • 首先在数据中寻找已排好序的连续子序列(称为“自然运行”)。
      • 如果 run 太短,使用二分插入排序(Binary Insertion Sort)扩展。
      • 合并这些 runs(类似归并排序),通过维护 run 的栈并遵循特定规则来平衡合并成本。
    • 设计目标:在真实世界数据(通常包含部分有序片段)上表现优异。


     

    TimSort 是一种混合稳定的排序算法,它结合了归并排序和插入排序。当合并两个已经有序的run时,算法需要逐个比较来自两个run的元素,以决定下一个元素应该放谁,从而保证合并后的序列仍然有序且稳定。这个过程是 顺序的、有状态的 ,后一步的决策依赖于前一步的结果。因此,很难将单个合并操作分解到多个线程中去并行处理而不产生巨大的同步开销。

    哪个“更好”取决于评判标准和应用场景:

    1. 对于大规模、随机的数据集,在多核CPU上, DualPivotQuicksort 通常更快。 因为它可以利用多核优势进行并行计算,这是它被选为Java基本类型数组(如 int[] , double[] )默认排序算法的原因。

    2. 对于部分有序的数据, TimSort 通常表现更好。 TimSort 被设计用来利用数据中已经存在的顺序,在这种情况下,它的比较次数远少于 n log n ,性能非常出色。现实世界中的很多数据都具有这种部分有序的特征。

    3. 当需要稳定排序时,必须使用 TimSort 。 稳定排序保证了相等元素的原始相对顺序在排序后不会改变。 DualPivotQuicksort 是不稳定的,而 TimSort 是稳定的。因此,Java中对象数组( Object[] )的 Arrays.sort() 和 Collections.sort() 都使用 TimSort 。


    总结对比表

    特性DualPivotQuicksortTimSort
    ​核心算法​双轴快速排序混合归并排序和插入排序
    ​稳定性​❌ 不稳定✔️ 稳定
    ​最坏时间复杂度​O(n²)O(n log n)
    ​平均时间复杂度​O(n log n)O(n log n)
    ​最好时间复杂度​O(n log n)O(n)
    ​空间复杂度​O(log n)O(n)
    ​JDK 用途​Arrays.sort(基本类型)Arrays.sort/Collections.sort(对象)

    结论

    • ​DualPivotQuicksort​​:
      适用于基本类型,追求极致平均性能且不要求稳定性。
    • ​TimSort​​:
      适用于对象排序,需稳定性和有保障的最坏情况性能。

    本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
    如若转载,请注明出处:http://www.pswp.cn/web/89434.shtml
    繁体地址,请注明出处:http://hk.pswp.cn/web/89434.shtml
    英文地址,请注明出处:http://en.pswp.cn/web/89434.shtml

    如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

    相关文章

    企业数据生命周期安全架构设计

    数据是企业的生命线&#xff0c;而安全则是这条生命线的保护神。今天我们就来聊聊如何为企业数据的一生一世构建一套坚不可摧的安全防护体系。 &#x1f4da; 文章目录 为什么需要数据生命周期安全架构数据生命周期全景图安全架构设计的核心原则各阶段安全防护策略整体安全架构…

    【Java】字符串常量池

    文章目录一.字符串常量池(StringTable)1.1 定义1.2 演示示例1.3 intern方法一.字符串常量池(StringTable) 1.1 定义 字符串常量词本质是一个固定大小的HashTable。当用一个字符串构造String对象时&#xff0c;首先会去StringTable中查看是否存在在字符串&#xff0c;如果存在…

    数据通信与计算机网络——模拟传输

    主要内容数字到模拟转换幅移键控ASK频移键控FSK相移键控PSK正交振幅调制QAM模拟信号调制调幅AM调频FM调相PM一、数字到模拟转换数字信号需要低通通道&#xff0c;如果现实应用中只有带通通道&#xff0c;只能选择模拟信号进行传输。将数字数据转换为带通模拟信号&#xff0c;传…

    如何用Python并发下载?深入解析concurrent.futures 与期物机制

    concurrent.futures模块的核心价值 Python的concurrent.futures模块提供了线程池&#xff08;ThreadPoolExecutor&#xff09;和进程池&#xff08;ProcessPoolExecutor&#xff09;两种并发模型&#xff0c;通过高层接口简化并发编程。其核心优势在于&#xff1a; 自动管理资源…

    MMKV 存储json list数据(kotlin)

    1、添加依赖与初始化 首先在 build.gradle 中添加 MMKV 依赖: implementationcom.tencent:mmkv:1.2.12 在 Application 类中初始化 MMKV: import android.app.Application import com.tencent.mmkv.MMKVclass MyApp : Application() { override fun onCreate() { super.o…

    C++ -- STL-- stack and queue

    ////// 欢迎来到 aramae 的博客&#xff0c;愿 Bug 远离&#xff0c;好运常伴&#xff01; ////// 博主的Gitee地址&#xff1a;阿拉美 (aramae) - Gitee.com 时代不会辜负长期主义者&#xff0c;愿每一个努力的人都能达到理想的彼岸。1. stack的介绍和使用 2. queue的介绍…

    信息论至AI实践:交叉熵的原理全景与应用深度解析

    1 定义与数学原理&#xff1a;从信息论到分布差异度量 交叉熵&#xff08;Cross Entropy&#xff09;是信息论中用于量化两个概率分布差异的核心概念&#xff0c;由Claude Shannon的信息论发展而来。它测量了在相同事件集合上&#xff0c;使用估计的概率分布q对服从真实概率分…

    WAF 能防御哪些攻击?

    WAF&#xff08;Web 应用防火墙&#xff09;是网站和Web应用的安全守门人&#xff0c;但很多用户对其具体防御范围一知半解。实际上&#xff0c;WAF 能针对性拦截多种网络攻击&#xff0c;从常见的注入攻击到复杂的恶意爬虫&#xff0c;覆盖Web安全的核心威胁。本文详解WAF的防…

    闲庭信步使用图像验证平台加速FPGA的开发:第二十二课——图像直方图统计的FPGA实现

    &#xff08;本系列只需要modelsim即可完成数字图像的处理&#xff0c;每个工程都搭建了全自动化的仿真环境&#xff0c;只需要双击top_tb.bat文件就可以完成整个的仿真&#xff0c;大大降低了初学者的门槛&#xff01;&#xff01;&#xff01;&#xff01;如需要该系列的工程…

    群晖中相册管理 immich大模型的使用

    相对于其他的相册管理软件&#xff0c;Immich的智能搜索和人脸识别功能是其优势&#xff0c;通过应用机器学习模型&#xff0c;其智能搜索和人脸识别功能更为先进。 一、大模型的下载与安装 网上有大佬提供了相关大模型的下载&#xff1a;https://url22.ctfile.com/d/58003522…

    在 Windows 上使用 Docker 运行 Elastic Open Crawler

    作者&#xff1a;来自 Elastic Matt Nowzari 了解如何使用 Docker 在 Windows 环境中运行 Open Crawler。 了解将数据摄取到 Elasticsearch 的不同方式&#xff0c;并深入实践示例&#xff0c;尝试一些新方法。 Elasticsearch 拥有大量新功能&#xff0c;助你为特定场景构建最…

    iOS高级开发工程师面试——RunTime

    iOS高级开发工程师面试——RunTime 一、简介 二、介绍下 RunTime 的内存模型(isa、对象、类、metaclass、结构体的存储信息等) 对象 类 三、为什么要设计 metaclass ? 四、class_copyIvarList & class_copyPropertyList区别? 五、class_rw_t 和 class_ro_t 的区别? 六…

    实现分页查询

    分页查询分页查询语句项目中添加分页功能按钮设置前后端代码功能实现分页查询语句 限制查询的 sql 语句&#xff1a; select * from student limit 0,4sql 查询结果如下&#xff1a; 分页查询的每一页都对应一行 sql 语句&#xff0c;若每一行都写单独对应的 sql 语句不仅重复…

    [QOI] qoi_desc | qoi_encode | qoi_decode

    链接&#xff1a;https://phoboslab.org/log/2021/11/qoi-fast-lossless-image-compression &#xff08;看代码设计的时候&#xff0c;真的大为震撼&#xff0c;伟大的algorithm T.T&#xff09; docs&#xff1a;QOI图像格式 qoi项目提出了Quite OK Image&#xff08;QOI&am…

    智慧城轨可视化:一屏智管全城

    图扑智慧城轨可视化系统&#xff0c;把地铁线路、车站、列车都搬进三维画面。列车晚点预警、站台拥挤提示、设备故障定位…… 这些关键信息一屏聚合&#xff0c;调度员能快速调整发车频次&#xff0c;疏导高峰客流。遇上突发情况&#xff0c;系统联动应急方案&#xff0c;同步显…

    包新的Git安装与使用教程(2024九月更新)

    目录 一、安装git 1.下载git 2.git安装 3.环境变量配置与测试 二、使用教程 1.创建版本库 2.版本回退 3.删除和恢复文件 一、安装git 1.下载git 官方下载地址&#xff1a;https://git-scm.com/download 然后进入以下页面&#xff0c;点击下载链接即可(windows一般都是…

    中望3D 2026亮点速递(1)-全新槽功能螺纹功能,减少繁琐操作

    本文为CAD芯智库整理&#xff0c;未经允许请勿复制、转载&#xff01;中望3D 2026全新的槽功能&#xff0c;包括&#xff1a;&#xff08;1&#xff09;可快速生成多种槽形&#xff1b;&#xff08;2&#xff09;快速生成一个或多个槽&#xff1b;&#xff08;3&#xff09;支持…

    2025毫米波雷达技术白皮书:智能汽车与物联网的感知核心

    随着人工智能、物联网&#xff08;IoT&#xff09;和智能汽车产业的迅猛发展&#xff0c;毫米波雷达技术正成为感知领域的核心驱动力。毫米波雷达凭借其高精度、全天候和强抗干扰能力&#xff0c;广泛应用于智能汽车的自动驾驶、物联网的环境感知以及工业自动化。2025年&#x…

    用 React-Three-Fiber 实现雪花下落与堆积效果:从零开始的 3D 雪景模拟

    在 Web3D 开发中&#xff0c;自然现象模拟一直是极具吸引力的主题。本文将基于 React-Three-Fiber&#xff08;R3F&#xff09;框架&#xff0c;详解如何实现一个包含雪花下落、地面堆积的完整雪景效果。我们会从基础粒子系统入手&#xff0c;逐步完善物理交互逻辑&#xff0c;…

    从抓包GitHub Copilot认证请求,认识OAuth 2.0技术

    引言 在现代开发工具中&#xff0c;GitHub Copilot 以智能、嵌入式的人工智能代码补全能力著称。作为一项涉及用户敏感数据和付费授权的服务&#xff0c;其认证授权流程尤为值得技术研究。本文基于实际抓包 VS Code 中的 Copilot 登录认证请求&#xff0c;系统梳理其 OAuth 2.…