乐于分享
好东西不私藏

Java源码中的排序算法(三)-TimSort

Java源码中的排序算法(三)-TimSort

在文章 Arrays.sort() 中提到引用类型使用了TimSort,现在就来揭开 Timsort 的神秘面纱。

“

“I believe that lists very often do have exploitable partial order in real life, and this is the strongest argument in favor of timsort”—— Tim Peters(Timsort 作者)

通过阅读做作者原始的设计文档,文中也一语道破TimSort的核心设计理念(公众号发送TimSort 获取原始设计文档)

”

https://svn.python.org/projects/python/trunk/Objects/listsort.txt?spm=5176.28103460.0.0.38f97d83P1I65F&file=listsort.txt

一、Timsort 是什么?

Timsort 是一种自适应的、稳定的归并排序变体,由 Python 社区的 Tim Peters 于 2002 年设计。如今,它已成为多个主流语言的标准排序算法:

在 JDK 中,当你对对象数组(如 String[]、List<Employee>)排序时,底层调用的就是 java.util.TimSort。而基本类型数组(如 int[])则使用 DualPivotQuicksort——因为对象排序必须稳定,而基本类型不需要。


二、核心逻辑:四步走策略

Timsort 的核心思想是:识别并合并数据中已有的有序片段(run),而非盲目分割。整个过程分为四个阶段:

1️⃣ 扫描并识别自然 run

从左到右扫描数组,找出天然存在的升序或降序子序列(称为 run)。如果是降序,立即反转为升序。

2️⃣ 扩展短 run 至最小长度

如果 run 长度小于 MIN_MERGE(JDK 21 中为 32),则用二分插入排序将其扩展到至少该长度。

3️⃣ 用栈管理 run 并智能合并

将 run 压入栈,维护两个不变式确保合并操作平衡:

  • runLen[i-3] > runLen[i-2] + runLen[i-1]
  • runLen[i-2] > runLen[i-1]

4️⃣ 归并时启用 galloping 模式

当一个 run 的多个连续元素整体小于另一个 run 时,切换到指数搜索 + 二分查找的 galloping 模式,大幅减少比较次数。


三、核心逻辑的核心代码(JDK 21)

1. 识别并规范化 run

// TimSort.javaprivatestatic <T> intcountRunAndMakeAscending(T[] a, int lo, int hi, Comparator<? super T> c){assert lo < hi;int runHi = lo + 1;if (runHi == hi) return1;// 判断是升序还是降序if (c.compare(a[runHi++], a[lo]) < 0) { // 降序while (runHi < hi && c.compare(a[runHi], a[runHi - 1]) < 0)            runHi++;        reverseRange(a, lo, runHi); // 反转为升序    } else { // 升序while (runHi < hi && c.compare(a[runHi], a[runHi - 1]) >= 0)            runHi++;    }return runHi - lo;}

“

💡 关键点:直接利用数据中的有序性,避免无谓分割。

”


2. 二分插入排序(扩展短 run)

privatestatic <T> voidbinarySort(T[] a, int lo, int hi, int start, Comparator<? super T> c){assert lo <= start && start <= hi;if (start == lo) start++;for (; start < hi; start++) {        T pivot = a[start];int left = lo, right = start;// 二分查找插入位置while (left < right) {int mid = (left + right) >>> 1;if (c.compare(pivot, a[mid]) < 0)                right = mid;else                left = mid + 1;        }// 移动元素并插入        System.arraycopy(a, left, a, left + 1, start - left);        a[left] = pivot;    }}

“

为什么用二分插入?对小数组(<32 元素),它比归并更高效,且缓存友好。

”


作者 Tim Peters 的原始设计文档

3. 智能合并策略(维护栈不变式)

privatevoidmergeCollapse(){while (stackSize > 1) {int n = stackSize - 2;// 检查是否违反不变式if ((n >= 1 && runLen[n-1] <= runLen[n] + runLen[n+1]) ||            (n >= 2 && runLen[n-2] <= runLen[n] + runLen[n-1])) {if (runLen[n - 1] < runLen[n + 1]) n--;        } elseif (runLen[n] > runLen[n + 1]) {break; // 不变式满足,退出        }        mergeAt(n); // 合并 run[n] 和 run[n+1]    }}

“

🛡️ 不变式的意义:防止大 run 合并小 run 导致性能退化,保证 O(n log n) 最坏情况。

”


4. Galloping 模式(加速归并)

privatevoidmergeLo(int base1, int len1, int base2, int len2){// ...int minGallop = this.minGallop;    outer:while (true) {// 正常模式:逐个比较// ...// 进入 galloping 模式do {assert len1 > 0 && len2 > 0;            count1 = gallopRight(a[base2], tmp, cursor1, len1, 0, c);// ... 处理 count1            count2 = gallopLeft(tmp[cursor1], a, base2, len2, 0, c);// ... 处理 count2if (count1 < MIN_GALLOP && count2 < MIN_GALLOP) {break outer; // 退出 galloping            }        } while (count1 >= MIN_GALLOP || count2 >= MIN_GALLOP);    }}

四、为什么这么做?—— 设计哲学

1. 真实数据不是随机的

现实世界的数组往往包含:

  • 已排序的子序列(如数据库按 ID 插入的记录)
  • 重复元素(如状态字段)
  • 局部有序(如按天分组的日志)

2. 稳定性是对象排序的生命线

想象一个场景:

// 先按部门排序employees.sort(Comparator.comparing(Employee::getDept));// 再按姓名排序employees.sort(Comparator.comparing(Employee::getName));

如果排序不稳定,第一次排序的结果会被第二次完全打乱。Timsort 保证稳定性,使多级排序成为可能。

3. 最坏情况不能妥协

快速排序在最坏情况下退化为 O(n²),而 Timsort 通过智能合并策略,严格保证 O(n log n),避免生产环境雪崩。

以上就是对TimSort的整体了解,后续我还将画一些时间深入学习它是怎么识别有序、扩展短run等核心步骤的,尽情关注

“

💡 感谢你看完这篇内容,这是我自己在工作学习中遇到的case,做一些简单的 究,并总结经验,如有遗漏或不合理的地方,欢迎你提出问题,让我们一起探索。

”

本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » Java源码中的排序算法(三)-TimSort

评论 抢沙发

5 + 5 =
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
×
订阅图标按钮