当前位置: 首页 > news >正文

Java 中 Arrays.sort() 的底层实现

文章目录

  • Java 中 Arrays.sort()的底层实现
    • 1.基本类型排序:双轴快速排序(Dual-Pivot Quicksort)
    • 2.对象类型排序:TimSort(归并排序 + 插入排序混合)
    • 3.并行排序:`Arrays.parallelSort()`

Java 中 Arrays.sort()的底层实现

image-20251110181751521

Arrays.sort() 是 Java 中最常用的排序方法之一,但它的底层实现其实 并不只有一种,而是根据 数据类型数据规模 来动态选择不同的排序算法。

数据类型使用的排序算法是否稳定底层类
基本类型(int、double、char 等)双轴快速排序(Dual-Pivot Quicksort)DualPivotQuicksort
对象类型(如 StringInteger、自定义类等)TimSort(归并+插入排序混合)TimSort
并行排序并行归并排序(ForkJoin 框架)Arrays.parallelSort()

1.基本类型排序:双轴快速排序(Dual-Pivot Quicksort)

DualPivotQuicksort 是改进版快速排序。与传统单轴快排不同,它使用 两个枢轴(pivot1 和 pivot2) 来将数组划分为 三个区域,然后递归地分别排序这三个区域。

[ < pivot1 ] [ pivot1 ≤ x ≤ pivot2 ] [ > pivot2 ]

平均时间复杂度:O(n log n)

源码原理

  • 对于小数组(长度小于 QUICKSORT_THRESHOLD(286)),直接使用快速排序。
    • 对于小数组(长度小于 INSERTION_SORT_THRESHOLD(47)),使用插入排序。
      • leftmosttrue 时,使用 传统插入排序
      • leftmostfalse 时,使用优化的 成对插入排序。(利用相邻部分作为哨兵)
    • 否则,使用快速排序。 从数组中均匀选取 5 个样本元素(包括中间元素)对这些样本元素进行排序,根据样本元素的分布情况决定使用双轴还是单轴策略。
      • 双轴快速排序逻辑:使用两个枢轴(pivot1pivot2,其中 pivot1 <= pivot2)。将数组分成三部分:小于 pivot1、介于 pivot1pivot2 之间、大于 pivot2。递归排序这三个部分。
      • 单轴快速排序逻辑:当数组中存在大量重复元素时使用。将数组分成三部分:小于枢轴、等于枢轴、大于枢轴,只需要递归排序小于和大于部分,提高效率。
  • 对于较大数组,首先尝试识别数组中的有序段(run),然后进行合并排序。
    • 扫描数组,识别其中的有序段(升序、降序或相等元素序列),对于降序段,会将其转换为升序,对于相等元素过多的情况,直接回退到快速排序,如果识别出的有序段数量超过 MAX_RUN_COUNT,也回退到快速排序。(预处理阶段
    • 检查特殊情况(如数组已排序或只有一个元素),确定归并排序的基础(使用临时数组或原数组),根据需要创建或使用工作数组。(归并排序准备
    • 成对合并已识别的有序段,在原数组和临时数组之间交替进行归并。(归并阶段

源码注释如下

/*** 对数组的指定范围进行排序,尽可能使用给定的工作数组片段进行合并操作** @param a 要排序的数组* @param left 要排序的第一个元素的索引(包含)* @param right 要排序的最后一个元素的索引(包含)* @param work 工作数组(片段)* @param workBase 工作数组中可用空间的起始位置* @param workLen 工作数组的可用大小*/
static void sort(int[] a, int left, int right, int[] work, int workBase, int workLen) {// 对于小数组,使用快速排序// 小数组使用快速排序效率更高,避免归并排序的额外开销if (right - left < QUICKSORT_THRESHOLD) {// 调用内部的快速排序方法sort(a, left, right, true);return;}/** run[i] 存储第i个有序段的起始索引* 有序段可以是升序或降序序列*/// 创建存储有序段起始位置的数组int[] run = new int[MAX_RUN_COUNT + 1];int count = 0; // 有序段计数run[0] = left; // 第一个有序段从left开始// 检查数组是否接近有序,识别所有有序段for (int k = left; k < right; run[count] = k) {if (a[k] < a[k + 1]) { // 升序序列// 扩展升序序列,直到遇到降序或结束while (++k <= right && a[k - 1] <= a[k]);} else if (a[k] > a[k + 1]) { // 降序序列// 扩展降序序列,直到遇到升序或结束while (++k <= right && a[k - 1] >= a[k]);// 将降序序列反转成升序for (int lo = run[count] - 1, hi = k; ++lo < --hi; ) {int t = a[lo]; a[lo] = a[hi]; a[hi] = t;}} else { // 相等元素序列// 处理相等元素的情况// 限制相等元素序列的最大长度,超过阈值则切换到快速排序for (int m = MAX_RUN_LENGTH; ++k <= right && a[k - 1] == a[k]; ) {if (--m == 0) {sort(a, left, right, true);return;}}}/** 如果识别的有序段数量超过最大值,说明数组结构不够好* 此时使用快速排序替代归并排序更高效*/if (++count == MAX_RUN_COUNT) {sort(a, left, right, true);return;}}// 检查特殊情况// 注意:此处变量"right"会增加1if (run[count] == right++) { // 最后一个有序段只包含一个元素run[++count] = right;} else if (count == 1) { // 整个数组已经排序return;}// 确定归并的交替基础// 通过计算确定最终归并结果应该放在哪个数组byte odd = 0;for (int n = 1; (n <<= 1) < count; odd ^= 1);// 使用或创建用于归并的临时数组bint[] b;                 // 临时数组,与a交替使用int ao, bo;              // 相对于'left'的数组偏移量int blen = right - left; // b所需的空间大小// 检查工作数组是否可用,否则创建新的工作数组if (work == null || workLen < blen || workBase + blen > work.length) {work = new int[blen];workBase = 0;}// 根据前面计算的odd值,决定归并的起始数组if (odd == 0) {// 将原数组内容复制到工作数组,原数组将用作目标数组System.arraycopy(a, left, work, workBase, blen);b = a;bo = 0;a = work;ao = workBase - left;} else {// 使用工作数组作为目标数组b = work;ao = 0;bo = workBase - left;}// 归并过程:循环合并有序段,直到只剩下一个有序段for (int last; count > 1; count = last) {// 两两合并有序段for (int k = (last = 0) + 2; k <= count; k += 2) {int hi = run[k];   // 当前要合并的两个段的结束位置int mi = run[k - 1]; // 两个段的中间位置// 执行合并操作,将两个有序段合并为一个for (int i = run[k - 2], p = i, q = mi; i < hi; ++i) {// 比较两个段的元素,选择较小的放入目标数组if (q >= hi || p < mi && a[p + ao] <= a[q + ao]) {b[i + bo] = a[p++ + ao];} else {b[i + bo] = a[q++ + ao];}}run[++last] = hi; // 记录合并后的段结束位置}// 处理奇数个有序段的情况,将最后一个未合并的段直接复制到目标数组if ((count & 1) != 0) {for (int i = right, lo = run[count - 1]; --i >= lo;b[i + bo] = a[i + ao]);run[++last] = right;}// 交换数组引用,为下一轮合并做准备int[] t = a; a = b; b = t;int o = ao; ao = bo; bo = o;}
}
/*** 使用双枢轴快速排序算法对数组的指定范围进行排序** @param a 要排序的数组* @param left 要排序的第一个元素的索引(包含)* @param right 要排序的最后一个元素的索引(包含)* @param leftmost 指示此部分是否在整个排序范围的最左侧*/
private static void sort(int[] a, int left, int right, boolean leftmost) {int length = right - left + 1;// 对于极小数组,使用插入排序// 小数组使用插入排序效率更高,因为其常数因子小if (length < INSERTION_SORT_THRESHOLD) {if (leftmost) {/** 对最左侧部分使用传统(无哨兵)的插入排序,* 此实现针对服务器VM进行了优化*/for (int i = left, j = i; i < right; j = ++i) {int ai = a[i + 1];// 将当前元素插入到前面已排序部分的正确位置while (ai < a[j]) {a[j + 1] = a[j]; // 元素右移if (j-- == left) { // 到达左边界break;}}a[j + 1] = ai; // 插入元素}} else {/** 跳过最长的升序序列*/do {if (left >= right) {return;}} while (a[++left] >= a[left - 1]);/** 利用相邻部分的元素作为哨兵,避免每次迭代的左边界检查* 此外,使用优化的成对插入排序算法,在快速排序的上下文中* 比传统插入排序实现更快*/for (int k = left; ++left <= right; k = ++left) {int a1 = a[k], a2 = a[left];// 确保a1 >= a2if (a1 < a2) {a2 = a1; a1 = a[left];}// 将较大的元素a1插入到正确位置while (a1 < a[--k]) {a[k + 2] = a[k];}a[++k + 1] = a1;// 将较小的元素a2插入到正确位置while (a2 < a[--k]) {a[k + 1] = a[k];}a[k + 1] = a2;}// 处理最后一个元素int last = a[right];while (last < a[--right]) {a[right + 1] = a[right];}a[right + 1] = last;}return;}// 计算length/7的近似值,使用位运算提高效率int seventh = (length >> 3) + (length >> 6) + 1;/** 对范围中心及其周围均匀分布的五个元素进行排序* 这些元素将用于下面的枢轴选择* 元素间距的选择是通过实验确定的,适用于各种输入*/int e3 = (left + right) >>> 1; // 中心点int e2 = e3 - seventh;int e1 = e2 - seventh;int e4 = e3 + seventh;int e5 = e4 + seventh;// 使用插入排序对这五个样本元素进行排序// 通过一系列比较和交换确保这五个元素有序if (a[e2] < a[e1]) { int t = a[e2]; a[e2] = a[e1]; a[e1] = t; }if (a[e3] < a[e2]) { int t = a[e3]; a[e3] = a[e2]; a[e2] = t;if (t < a[e1]) { a[e2] = a[e1]; a[e1] = t; }}if (a[e4] < a[e3]) { int t = a[e4]; a[e4] = a[e3]; a[e3] = t;if (t < a[e2]) { a[e3] = a[e2]; a[e2] = t;if (t < a[e1]) { a[e2] = a[e1]; a[e1] = t; }}}if (a[e5] < a[e4]) { int t = a[e5]; a[e5] = a[e4]; a[e4] = t;if (t < a[e3]) { a[e4] = a[e3]; a[e3] = t;if (t < a[e2]) { a[e3] = a[e2]; a[e2] = t;if (t < a[e1]) { a[e2] = a[e1]; a[e1] = t; }}}}// 设置指针int less  = left;  // 中间部分第一个元素的索引int great = right; // 右侧部分第一个元素之前的索引// 如果五个样本元素都不相等,则使用双枢轴排序if (a[e1] != a[e2] && a[e2] != a[e3] && a[e3] != a[e4] && a[e4] != a[e5]) {/** 使用五个已排序元素中的第二个和第四个作为枢轴* 这些值是数组第一和第二个三分位数的近似值* 注意pivot1 <= pivot2*/int pivot1 = a[e2];int pivot2 = a[e4];/** 将待排序的第一个和最后一个元素移到枢轴原来的位置* 当分区完成后,枢轴将交换回它们的最终位置,* 并排除在后续排序之外*/a[e2] = a[left];a[e4] = a[right];/** 跳过已经小于pivot1或大于pivot2的元素*/while (a[++less] < pivot1);while (a[--great] > pivot2);/** 分区过程:**   左侧部分            中间部分                    右侧部分* +--------------------------------------------------------------+* |  < pivot1  |  pivot1 <= && <= pivot2  |    ?    |  > pivot2  |* +--------------------------------------------------------------+*               ^                          ^       ^*               |                          |       |*              less                        k     great** 不变式:**              所有在(left, less)中的元素   < pivot1*    pivot1 <= 所有在[less, k)中的元素     <= pivot2*              所有在(great, right)中的元素 > pivot2** 指针k是?-部分的第一个索引*/outer:for (int k = less - 1; ++k <= great; ) {int ak = a[k];if (ak < pivot1) { // 将a[k]移到左侧部分a[k] = a[less];/** 这里和下面使用"a[i] = b; i++"而不是* "a[i++] = b"是出于性能考虑*/a[less] = ak;++less;} else if (ak > pivot2) { // 将a[k]移到右侧部分while (a[great] > pivot2) {if (great-- == k) {break outer;}}if (a[great] < pivot1) { // a[great] <= pivot2a[k] = a[less];a[less] = a[great];++less;} else { // pivot1 <= a[great] <= pivot2a[k] = a[great];}/** 这里和下面使用"a[i] = b; i--"而不是* "a[i--] = b"是出于性能考虑*/a[great] = ak;--great;}}// 将枢轴交换到它们的最终位置a[left]  = a[less  - 1]; a[less  - 1] = pivot1;a[right] = a[great + 1]; a[great + 1] = pivot2;// 递归排序左侧和右侧部分,排除已知的枢轴sort(a, left, less - 2, leftmost);sort(a, great + 2, right, false);/** 如果中间部分太大(占数组的比例超过4/7),* 将内部的枢轴值交换到两端*/if (less < e1 && e5 < great) {/** 跳过等于枢轴值的元素*/while (a[less] == pivot1) {++less;}while (a[great] == pivot2) {--great;}/** 分区:**   左侧部分          中间部分                   右侧部分* +----------------------------------------------------------+* | == pivot1 |  pivot1 < && < pivot2  |    ?    | == pivot2 |* +----------------------------------------------------------+*              ^                        ^       ^*              |                        |       |*             less                      k     great** 不变式:**              所有在(*, less)中的元素 == pivot1*     pivot1 < 所有在[less, k)中的元素  < pivot2*              所有在(great, *)中的元素 == pivot2** 指针k是?-部分的第一个索引*/outer:for (int k = less - 1; ++k <= great; ) {int ak = a[k];if (ak == pivot1) { // 将a[k]移到左侧部分a[k] = a[less];a[less] = ak;++less;} else if (ak == pivot2) { // 将a[k]移到右侧部分while (a[great] == pivot2) {if (great-- == k) {break outer;}}if (a[great] == pivot1) { // a[great] < pivot2a[k] = a[less];/** 尽管a[great]等于pivot1,但赋值a[less] = pivot1* 可能不正确,如果a[great]和pivot1是符号不同的浮点零* 因此在float和double排序方法中,我们必须使用更准确的* 赋值a[less] = a[great]*/a[less] = pivot1;++less;} else { // pivot1 < a[great] < pivot2a[k] = a[great];}a[great] = ak;--great;}}}// 递归排序中间部分sort(a, less, great, false);} else { // 使用单枢轴分区/** 使用五个已排序元素中的第三个作为枢轴* 这个值是中位数的近似值*/int pivot = a[e3];/** 分区退化为传统的三向切分(或"荷兰国旗")模式:**   左侧部分    中间部分              右侧部分* +-------------------------------------------------+* |  < pivot  |   == pivot   |     ?    |  > pivot  |* +-------------------------------------------------+*              ^              ^        ^*              |              |        |*             less            k      great** 不变式:**   所有在(left, less)中的元素   < pivot*   所有在[less, k)中的元素     == pivot*   所有在(great, right)中的元素 > pivot** 指针k是?-部分的第一个索引*/for (int k = less; k <= great; ++k) {if (a[k] == pivot) {continue;}int ak = a[k];if (ak < pivot) { // 将a[k]移到左侧部分a[k] = a[less];a[less] = ak;++less;} else { // a[k] > pivot - 将a[k]移到右侧部分while (a[great] > pivot) {--great;}if (a[great] < pivot) { // a[great] <= pivota[k] = a[less];a[less] = a[great];++less;} else { // a[great] == pivot/** 尽管a[great]等于pivot,但赋值a[k] = pivot* 可能不正确,如果a[great]和pivot是符号不同的浮点零* 因此在float和double排序方法中,我们必须使用更准确的* 赋值a[k] = a[great]*/a[k] = pivot;}a[great] = ak;--great;}}/** 递归排序左侧和右侧部分* 中间部分的所有元素都相等,因此已经排序*/sort(a, left, less - 1, leftmost);sort(a, great + 1, right, false);}
}

2.对象类型排序:TimSort(归并排序 + 插入排序混合)

TimSort 是一种 混合排序算法,它结合了 归并排序(稳定、高效)插入排序(对小规模数据高效) 的优点。

Java 在排序 Object[] 时调用:Arrays.sort(Object[] a) 底层实际上是:TimSort.sort(a, 0, a.length, null, 0, 0);

TimSort 先将数组划分成若干个小块(称为 run),每个 run 都是一个 已排序序列(自然有序或通过插入排序得到)。然后利用归并思想合并这些 run。

原数组 → 分割为 runs → 对每个 run 插入排序 → 合并相邻 runs(归并)

平均时间复杂度:O(n log n)

3.并行排序:Arrays.parallelSort()

底层使用 ForkJoin 框架 实现并行归并排序:

将数组递归地分割为多个子数组 -> 每个子任务并行排序 -> 使用归并算法合并结果

平均时间复杂度:O(n log n)

⬇️⬇️⬇️⬇️⬇️

总结:Arrays.sort() 对基本类型用双轴快排,对对象类型用 TimSort,对大数组可以用 parallelSort 并行归并。

🐮🐴

http://www.dtcms.com/a/592943.html

相关文章:

  • MPAndroidChart 双柱分组图:解决 X 轴标签无法居中问题及 UI 宽度计算指南
  • 政务外网终端一机两用安全管控解决方案
  • 数字华容道游戏
  • M4-R1 开源鸿蒙(OpenHarmory)开发板丨串口调试助手实战案例
  • 建材做网站好吗破解插件有后门wordpress
  • 旅游网站建设流程步骤怎么自己做礼品网站
  • C语言--文件读写函数的使用,对文件读写知识有了更深的了解。C语言--文件读写函数的使用,对文件读写知识有了更深的了解。
  • 数据结构示例代码
  • 数字化工厂:基于层级模型的智能制造新范式
  • C语言--变量(全局变量、局部变量、初始化)
  • 羊驼送洗后因毛发未吹干致失温死亡,物联网技术助力防范宠物洗澡失温事故
  • Raylib 基本绘图操作
  • (Arxiv-2025)BINDWEAVE:通过跨模态整合实现主体一致性的视频生成
  • 怎么做会员积分网站建网站商城有哪些公司
  • 网站如何验证登陆状态广州专业做网页的公司
  • MySQL的增删改查功能合集
  • Oracle数据块编辑工具( Oracle Block Editor Tool)-obet
  • 什么是量子纠缠?大白话
  • 用服务器自建 RustDesk 远程控制平台
  • 新手做网站如何被百度快速收录教程
  • 基于java技术的田径俱乐部网站的设计与实现
  • 第二十四篇:C++模板元编程入门:constexpr与type_traits的编译期魔法
  • C语言数组作为函数参数(3种写法,附带实例)
  • SPARQL文档导读
  • JavaEE初阶——JUC的工具类和死锁
  • 如何将自己做的网站发布到网上ui展示 网站
  • 上门家政小程序用户激励机制分析:用 “利益 + 情感” 双驱动,解锁高复购增长密码
  • 内网横向靶场——记录一次横向渗透(二)
  • Mysql作业四
  • 枣庄住房和城乡建设厅网站教育网站制作开发