深入剖析JDK源码之Arrays.sort排序算法

news2025/1/10 16:45:37

一、引言

在 Java 编程的日常开发中,对数组进行排序是一项极为常见的操作。无论是处理简单的数据列表,还是应对复杂的数据结构,我们常常会依赖 java.util.Arrays 类中的 sort() 方法来轻松实现数组的排序需求。这个方法就像是一个万能的工具,只需一行代码,就能将数组元素按照特定的顺序排列整齐,为后续的数据处理和算法实现提供了极大的便利。但你是否曾好奇,这看似简单的 Arrays.sort() 背后,隐藏着怎样精妙复杂的排序算法呢?深入探究其源码,不仅能够满足我们对知识的渴望,更能让我们在面对各种复杂的排序场景时,精准地选择最合适的排序策略,优化程序性能。接下来,就让我们一同揭开 Arrays.sort() 排序算法源码的神秘面纱。

二、Arrays.sort () 方法入口

Arrays.sort() 方法位于 java.util.Arrays 类中,它提供了多个重载方法,以适应不同类型的数组以及各种排序需求。对于基本数据类型的数组,如 int[]double[]char[] 等,有对应的 sort() 方法;对于对象数组,若对象实现了 Comparable 接口,也能直接使用 sort() 方法进行排序,此外还可以传入自定义的比较器 Comparator 来实现特定的排序逻辑。以常见的 int[] 数组排序为例,当我们调用 Arrays.sort(int[] a) 时,其内部实际上是调用了 DualPivotQuicksort.sort() 方法来完成排序工作,如下所示:

import java.util.Arrays;

public class SortExample {

    public static void main(String[] args) {

        int[] array = {9, 4, 7, 2, 6, 1, 8};

        Arrays.sort(array);

        for (int num : array) {

            System.out.print(num + " ");

        }

    }

}

在上述代码中,Arrays.sort(array) 这一行就是触发排序的关键,它会引领我们深入到 Arrays.sort() 方法的底层实现,探寻排序的奥秘。

三、核心阈值剖析

3.1 阈值总览

在深入探究 Arrays.sort() 背后的排序算法时,我们会发现几个关键的阈值起着决定性的作用,它们就像是一个个精密的开关,根据数组的长度和类型,巧妙地调控着排序算法的选择,以实现最优的性能表现。这些阈值包括 QUICKSORT_THRESHOLDINSERTION_SORT_THRESHOLDCOUNTING_SORT_THRESHOLD_FOR_BYTE 以及 COUNTING_SORT_THRESHOLD_FOR_SHORT_OR_CHAR 等,每一个都蕴含着设计者对不同排序算法在不同场景下优劣的深刻洞察。接下来,让我们逐一剖析这些关键阈值,揭开 Arrays.sort() 高效排序的秘密。

3.2 快速排序阈值

QUICKSORT_THRESHOLD 被设定为 286,这是一个影响排序算法走向的重要标杆。当待排序数组的长度小于此阈值时,排序算法会优先选择快速排序,而非归并排序。这背后的原因在于,快速排序在处理小规模数据时,其平均性能表现卓越,能够以较低的时间复杂度迅速完成排序任务。相较于传统的单轴快排, Arrays.sort() 中采用的双轴快排算法更是在许多复杂数据集上展现出了强大的适应性,它能够有效避免其他快排算法在面对特定数据分布时可能出现的性能退化问题,始终保持着接近   的高效性能,为小规模数组的快速排序提供了坚实保障。

3.3 插入排序阈值

再看 INSERTION_SORT_THRESHOLD,其值为 47。当数组长度小于这个阈值时,插入排序将取代快速排序成为首选。尽管快速排序在大数据集上表现优异,但对于小规模数组,插入排序却有着独特的优势。插入排序的基本思想是将数据逐个插入到已经有序的部分中,对于元素较少的数组,其数据移动和比较的开销相对较小,而且代码实现简洁,无需复杂的分区操作,能够充分利用数组局部有序的特性,以线性时间复杂度快速完成排序,从而在小规模数据场景下实现更高的效率。

3.4 计数排序阈值(byte、char 类型)

对于 byte 类型的数组,当长度大于 COUNTING_SORT_THRESHOLD_FOR_BYTE(即 29)时,计数排序将大显身手,优先于插入排序被采用。计数排序的核心原理是利用 byte 类型数据取值范围有限(-128 到 127,共 256 个值)的特点,通过统计每个值出现的次数,然后按照顺序依次输出,从而实现高效排序。这种算法避免了数据之间的大量比较操作,对于大规模的 byte 数组,能够极大地提升排序速度,相较于插入排序在数据量较大时的频繁数据移动,具有显著的性能优势。

char 类型数组也有其对应的计数排序阈值 COUNTING_SORT_THRESHOLD_FOR_SHORT_OR_CHAR,设定为 3200。当 char 数组长度小于此阈值时,会优先选用双轴快排;反之,计数排序则成为更优选择。这是因为 char 类型数据同样具有相对固定的取值范围(0 到 65535),在大规模数据且取值分布较为均匀的情况下,计数排序能够充分发挥其线性时间复杂度的优势,迅速完成排序任务,相比双轴快排的分治与递归操作,大大减少了排序时间。

四、排序算法详解

4.1 双轴快速排序

双轴快速排序(Dual-Pivot Quicksort)作为 Arrays.sort() 中的核心排序算法之一,相较于传统的单轴快速排序,有着显著的优化。它的核心思想是选取两个轴元素 pivot1 和 pivot2(且 pivot1 ≤ pivot2),通过一趟排序将序列分成三段:x < pivot1pivot1 ≤ x ≤ pivot2x > pivot2,然后分别对这三段进行递归排序。在选择基准值时,它会先从数组中选取五个等距的元素,对这五个元素进行插入排序后,选取其中的第二个和第四个元素作为 pivot1 和 pivot2,这样的基准值选取方式能够在一定程度上代表数组的整体分布,减少极端情况的出现。例如,对于数组 [9, 4, 7, 2, 6, 1, 8],假设选取的五个等距元素为 [2, 4, 6, 7, 9],经过插入排序后为 [2, 4, 6, 7, 9],此时 pivot1 = 4pivot2 = 7。接着,定义两个指针 less 和 greatless 从最左边开始向右遍历,找到第一个不小于 pivot1 的元素,great 从右边开始向左遍历,找到第一个不大于 pivot2 的元素,然后通过指针 k 对中间部分进行调整,使得左边部分都小于 pivot1,右边部分都大于 pivot2,最后将 pivot1 和 pivot2 放到合适的位置,并对三段分别递归排序。这种双轴划分的方式,相比单轴快排,能够更好地应对各种数据分布,在多数情况下保持接近   的时间复杂度,大大提高了排序效率。

4.2 插入排序

插入排序(Insertion Sort)的原理十分直观,它将数据看作是一个有序序列和一个无序序列,初始时,有序序列仅包含数组的第一个元素,然后逐个将无序序列中的元素插入到有序序列的合适位置。在 Arrays.sort() 中,当数组长度小于 INSERTION_SORT_THRESHOLD(即 47)时,会启用插入排序。这是因为对于小规模数组,插入排序的开销相对较小。以数组 [4, 2, 7, 1] 为例,初始时有序序列为 [4],然后将 2 插入到 [4] 中,得到 [2, 4],接着将 7 插入到 [2, 4] 中,依然是 [2, 4, 7],最后将 1 插入,得到 [1, 2, 4, 7]。而且,在一些接近有序的数组中,插入排序能够充分利用数组的有序特性,减少元素的比较和移动次数,以线性时间复杂度快速完成排序,避免了快速排序等算法在小规模数据上的额外开销。

4.3 计数排序

计数排序(Counting Sort)是一种基于统计思想的排序算法,它的前提是数据的取值范围相对固定且较小。对于 byte 类型数组,当长度大于 COUNTING_SORT_THRESHOLD_FOR_BYTE(即 29)时,计数排序就会发挥优势。因为 byte 类型数据取值范围是 -128 到 127,总共 256 个值。计数排序通过创建一个长度为 256 的计数数组 count,统计每个值出现的次数,然后按照计数数组的顺序依次输出,即可完成排序。例如,对于 byte 数组 [10, 20, 10, 30, 20],首先创建计数数组 count 并初始化为 0,遍历数组,当遇到 10 时,count[10 + 128]++,遇到 20 时,count[20 + 128]++,以此类推,统计完后,根据 count 数组依次输出对应次数的元素,就能得到有序数组。相比基于比较的排序算法,计数排序避免了大量的数据比较操作,在处理大规模且取值范围有限的数据时,时间复杂度可达到  (其中 n 是数据个数,k 是取值范围),大大提升了排序速度。

4.4 归并排序

归并排序(Merge Sort)是一种典型的分治算法,它的核心思想是将数组不断地分成两半,对每一半进行排序,然后再将排好序的两半合并起来。在 Arrays.sort() 中,当数组长度大于等于 QUICKSORT_THRESHOLD(即 286)时,会考虑使用归并排序。在合并阶段,需要申请额外的空间,使其大小为两个已排序序列之和,设定两个指针分别指向两个已排序序列的起始位置,比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置,重复此操作,直到某一指针达到序列尾,再将另一序列剩下的所有元素直接复制到合并序列尾。例如,对于数组 [9, 4, 7, 2, 6, 1, 8],先分成 [9, 4, 7] 和 [2, 6, 1, 8],对这两部分分别排序得到 [4, 7, 9] 和 [1, 2, 6, 8],然后合并这两个有序序列,最终得到 [1, 2, 4, 6, 7, 8, 9]。归并排序的优点是它的时间复杂度始终稳定在  ,无论数据的初始状态如何,都能保证稳定的排序性能,适合处理大规模的数组。

五、算法选择逻辑

了解了各种排序算法以及关键阈值后,我们来梳理一下 Arrays.sort() 方法中排序算法的选择逻辑。当面对一个待排序数组时,首先会判断数组的类型,对于基本数据类型如 intdouble 等,以及对象数组实现了 Comparable 接口的情况,进入不同的分支处理。以 int 数组为例,会先获取数组长度 n,若 n < INSERTION_SORT_THRESHOLD(即 47),则直接选用插入排序;若 47 <= n < QUICKSORT_THRESHOLD(即 286),则使用双轴快速排序;若 n >= 286,此时会先检查数组是否接近有序,通过遍历数组,将连续的升序、降序或相等的元素段标记为 “run”,统计 “run” 的数量 count,若 count > MAX_RUN_COUNT(即 67),说明数组无序程度较高,改用双轴快速排序,否则进入归并排序阶段。对于 byte 类型数组,若长度大于 COUNTING_SORT_THRESHOLD_FOR_BYTE(即 29),优先采用计数排序,否则用插入排序;char 类型数组,若长度小于 COUNTING_SORT_THRESHOLD_FOR_SHORT_OR_CHAR(即 3200),采用双轴快速排序,否则用计数排序。这种依据阈值、数组类型和长度的动态算法选择机制,充分发挥了各种排序算法的优势,使得 Arrays.sort() 在不同场景下都能高效运行。以下是一个简单的算法选择流程图,帮助大家更直观地理解:

@startuml

start

:输入待排序数组;

if (数组类型 == byte) then (yes)

    if (数组长度 > COUNTING_SORT_THRESHOLD_FOR_BYTE) then (yes)

        :采用计数排序;

    else (no)

        :采用插入排序;

    endif

elseif (数组类型 == char) then (yes)

    if (数组长度 < COUNTING_SORT_THRESHOLD_FOR_SHORT_OR_CHAR) then (yes)

        :采用双轴快速排序;

    else (no)

        :采用计数排序;

    endif

else (no)

    :计算数组长度n;

    if (n < INSERTION_SORT_THRESHOLD) then (yes)

        :采用插入排序;

    elseif (n < QUICKSORT_THRESHOLD) then (yes)

        :采用双轴快速排序;

    else (no)

        :检查数组是否接近有序,统计“run”数量count;

        if (count > MAX_RUN_COUNT) then (yes)

            :采用双轴快速排序;

        else (no)

            :采用归并排序;

        endif

    endif

endif

:完成排序;

stop

@enduml

通过这个流程图,我们可以清晰地看到 Arrays.sort() 在面对不同类型和长度的数组时,是如何精准地选择最合适的排序算法,以实现最优性能的。

六、实战案例分析

为了更直观地感受 Arrays.sort() 的强大与精妙,下面我们通过几个具体的实战案例来深入分析。

示例一:小规模整数数组排序

考虑数组 int[] smallArray = {23, 12, 35, 4, 18, 3},其长度为 6,小于 INSERTION_SORT_THRESHOLD(47)。此时,Arrays.sort() 会选用插入排序算法。排序过程如下:

初始时,有序序列为 [23],然后将 12 插入到 [23] 中,得到 [12, 23];接着将 35 插入,依然是 [12, 23, 35];再将 4 插入,通过比较,将 4 依次与 352312 比较并移动元素,得到 [4, 12, 23, 35];随后插入 18,得到 [4, 12, 18, 23, 35];最后插入 3,经过比较和移动,最终得到有序数组 [3, 4, 12, 18, 23, 35]。整个过程充分利用了小规模数组元素少、插入开销小的特点,快速完成排序。

示例二:中等规模随机整数数组排序

对于数组 int[] mediumArray = {56, 27, 89, 13, 42, 65, 37, 71, 9, 30},其长度为 10,满足 47 <= n < QUICKSORT_THRESHOLD(286),将采用双轴快速排序。假设选取的五个等距元素为 [13, 27, 42, 65, 89],经插入排序后不变,选取 pivot1 = 27pivot2 = 65。接着,从左至右遍历数组,将小于 27 的元素移到左边,大于 65 的元素移到右边,中间部分元素通过指针调整,最终得到三段:[9, 13, 27][30, 37, 42, 56][65, 71, 89],然后分别对这三段递归排序,即可得到有序数组。这种双轴划分有效应对了中等规模随机数据,保持了较高的排序效率。

示例三:大规模字节数组排序

假设有一个 byte[] largeByteArray,长度为 100,且包含大量重复元素,由于其长度大于 COUNTING_SORT_THRESHOLD_FOR_BYTE(29),计数排序将发挥优势。首先创建长度为 256 的计数数组 count,遍历 largeByteArray,统计每个字节值出现的次数。例如,若数组中有 10 个值为 10 的元素,那么 count[10 + 128] = 10。统计完成后,按照计数数组的顺序依次输出对应次数的元素,就能快速得到有序的字节数组,避免了大量数据比较操作,充分利用了字节取值范围有限的特性。

通过以上实战案例,我们可以清晰地看到 Arrays.sort() 在不同场景下,依据数组长度、类型等因素,灵活选用最合适的排序算法,高效完成排序任务,为我们的编程实践提供了强有力的支持。

七、自定义排序规则的坑

自定义排序时,常需实现 Comparator 接口,其中 compare 方法返回值意义重大,规定返回负数、0、正数分别对应首元素小于、等于、大于次元素。一旦刻意不返回 0,比如在某些复杂逻辑下持续返回非零值,当排序元素数量超过 32 个时,很可能触发内部校验异常。因为排序算法在某些优化分支或稳定性保障环节,依赖正确的相等关系判断,缺少准确的 “相等” 反馈,数据顺序就可能错乱,最终导致程序抛出难以预料的异常。

import java.util.Arrays;
import java.util.Comparator;

public class CustomSortTrap {
    public static void main(String[] args) {
        Integer[] values = new Integer[33];
        for (int i = 0; i < 33; i++) {
            values[i] = i;
        }
        Arrays.sort(values, new Comparator<Integer>() {
            @Override
            public int compare(Integer a, Integer b) {
                // 错误示范,从不返回0
                return a < b? -1 : 1;
            }
        });
    }
}

八、总结

通过对 Arrays.sort() 排序算法源码的深入探究,我们揭开了其背后复杂而精妙的排序机制。从双轴快速排序的高效分区,到插入排序对小规模数据的精准处理,再到计数排序针对特定数据类型的优化以及归并排序在大规模数据上的稳定表现,每一种算法都在各自擅长的场景中发挥着关键作用。而那些精心设定的阈值,更是如同精密的导航仪,根据数组的长度和类型,智能地引导程序选择最合适的排序路径,从而实现了整体性能的最优。对于广大 Java 开发者而言,深入理解这些细节,不仅能够在日常编程中更加得心应手地运用 Arrays.sort() 方法,还能在面对复杂的性能优化挑战时,借鉴其中的算法思想,精准地调整代码,提升程序的运行效率。希望这篇文章能成为大家探索 Java 底层奥秘的一把钥匙,开启更多高效编程的可能。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

el-table表格合并某一列

需求&#xff1a;按照下图完成单元格合并&#xff0c;数据展示 可以看到科室列是需要合并的 并加背景色展示&#xff1b;具体代码如下&#xff1a; <el-tableref"tableA":data"tableDataList":header-cell-style"{ backgroundColor: #f2dcdb, col…

PostgreSQL学习笔记(二):PostgreSQL基本操作

PostgreSQL 是一个功能强大的开源关系型数据库管理系统 (RDBMS)&#xff0c;支持标准的 SQL 语法&#xff0c;并扩展了许多功能强大的操作语法. 数据类型 数值类型 数据类型描述存储大小示例值SMALLINT小范围整数&#xff0c;范围&#xff1a;-32,768 到 32,7672 字节-123INTE…

javaEE-网络编程4.TCP回显服务器

目录 TCP流套接字编程 一.API介绍 ServerSocket类 构造方法&#xff1a; ​编辑方法&#xff1a; Socket类 构造方法&#xff1a; 方法&#xff1a; 二、TCP连接 三、通过TCP实现回显服务器 TCP服务端&#xff1a; 1.创建Socket对象 2.构造方法 3.start方法 TCP客…

RIS智能无线电反射面:原理、应用与MATLAB代码示例

一、引言 随着无线通信技术的快速发展,人们对通信系统的容量、覆盖范围、能效以及安全性等方面的要求日益提高。传统的无线通信系统主要通过增加基站数量、提高发射功率和优化天线阵列等方式来提升性能,但这些方法面临着资源有限、能耗高和成本上升等挑战。因此,探索新的无线…

合并模型带来的更好性能

研究背景与问题提出 在人工智能领域&#xff0c;当需要处理多个不同任务时&#xff0c;有多种方式来运用模型资源。其中&#xff0c;合并多个微调模型是一种成本效益相对较高的做法&#xff0c;相较于托管多个专门针对不同任务设计的模型&#xff0c;能节省一定成本。然而&…

城市生命线安全综合监管平台

【落地产品&#xff0c;有需要可留言联系&#xff0c;支持项目合作或源码合作】 一、建设背景 以关于城市安全的重要论述为建设纲要&#xff0c;聚焦城市安全重点领域&#xff0c;围绕燃气爆炸、城市内涝、地下管线交互风险、第三方施工破坏、供水爆管、桥梁坍塌、道路塌陷七…

Flink系列知识讲解之:网络监控、指标与反压

Flink系列知识之&#xff1a;网络监控、指标与反压 在上一篇博文中&#xff0c;我们介绍了 Flink 网络协议栈从高层抽象到底层细节的工作原理。本篇博文是网络协议栈系列博文中的第二篇&#xff0c;在此基础上&#xff0c;我们将讨论如何监控网络相关指标&#xff0c;以识别吞…

生物医学信号处理--随机信号的数字特征

前言 概率密度函数完整地表现了随机变量和随机过程的统计性质。但是信号经处理后再求其概率密度函数往往较难&#xff0c;而且往往也并不需要完整地了解随机变量或过程的全部统计性质只要了解其某些特定方面即可。这时就可以引用几个数值来表示该变量或过程在这几方面的特征。…

计算机网络 (31)运输层协议概念

一、概述 从通信和信息处理的角度看&#xff0c;运输层向它上面的应用层提供通信服务&#xff0c;它属于面向通信部分的最高层&#xff0c;同时也是用户功能中的最低层。运输层的一个核心功能是提供从源端主机到目的端主机的可靠的、与实际使用的网络无关的信息传输。它向高层用…

深度学习张量的秩、轴和形状

深度学习张量的秩、轴和形状 秩、轴和形状是在深度学习中我们最关心的张量属性。 秩轴形状 秩、轴和形状是在深度学习中开始使用张量时我们最关心的三个属性。这些概念相互建立&#xff0c;从秩开始&#xff0c;然后是轴&#xff0c;最后构建到形状&#xff0c;所以请注意这…

积分与签到设计

积分 在交互系统中&#xff0c;可以通过看视频、发评论、点赞、签到等操作获取积分&#xff0c;获取的积分又可以参与排行榜、兑换优惠券等&#xff0c;提高用户使用系统的积极性&#xff0c;实现引流。这些功能在很多项目中都很常见&#xff0c;关于功能的实现我的思路如下。 …

vue实现虚拟列表滚动

<template> <div class"cont"> //box 视图区域Y轴滚动 滚动的是box盒子 滚动条显示的也是因为box<div class"box">//itemBox。 一个空白的盒子 计算高度为所有数据的高度 固定每一条数据高度为50px<div class"itemBox" :st…

IEC61850遥控-增强安全选控是什么?

摘要&#xff1a;遥控服务是IEC61850协议中非常重要的一项服务&#xff0c;其通常会被应用在电源开关、指示灯、档位调节等器件的操作。 遥控是一类比较特殊的操作&#xff0c;其通过远程方式操作指定的设备器件&#xff0c;在一些重要的场景中需要有严谨的机制来进行约束&…

【Uniapp-Vue3】创建自定义页面模板

大多数情况下我们都使用的是默认模板&#xff0c;但是默认模板是Vue2格式的&#xff0c;如果我们想要定义一个Vue3模板的页面就需要自定义。 一、我们先复制下面的模板代码&#xff08;可根据自身需要进行修改&#xff09;&#xff1a; <template><view class"…

如何操作github,gitee,gitcode三个git平台建立镜像仓库机制,这样便于维护项目只需要维护一个平台仓库地址的即可-优雅草央千澈

如何操作github&#xff0c;gitee&#xff0c;gitcode三个git平台建立镜像仓库机制&#xff0c;这样便于维护项目只需要维护一个平台仓库地址的即可-优雅草央千澈 问题背景 由于我司最早期19年使用的是gitee&#xff0c;因此大部分仓库都在gitee有几百个库的代码&#xff0c;…

QThread多线程详解

本文结构如下 文章目录 本文结构如下 1.概述2.开始多线程之旅2.1应该把耗时代码放在哪里&#xff1f;2.2再谈moveToThread() 3.启动线程前的准备工作3.1开多少个线程比较合适&#xff1f;3.2设置栈大小 4.启动线程/退出线程4.1启动线程4.2优雅的退出线程 5.操作运行中的线程5.1…

深度学习数据集有没有规范或指导意见,数据集的建立都需要做哪些研究工作?

一、数据集的核心原则是什么&#xff1f; 数据集的目标&#xff1a;它需要回答“你要解决什么问题&#xff1f;” 在构建数据集之前&#xff0c;最重要的不是去采集数据&#xff0c;而是明确目标&#xff1a; 你的模型是要做图像分类&#xff0c;还是目标检测&#xff1f;是要…

前端for循环遍历——foreach、map使用

title: 前端不同类型的for循环遍历——foreach、map date: 2025-01-04 11:02:17 tags: javascript 前端不同类型的for循环遍历 场景&#xff1a;很多时候后端发来的数据是不能够完全契合前端的需求的&#xff0c;比如你要一个数据对象中的值&#xff0c;但是这个值却作为了ke…

MR30分布式 IO 在物流分拣线的卓越应用

在当今物流行业高速发展的时代&#xff0c;物流分拣线的高效与精准运作至关重要&#xff0c;而其中对于货物点数较多情况下的有效控制更是一大关键环节。明达技术MR30分布式 IO 系统凭借其独特的优势&#xff0c;在物流分拣线中大放异彩&#xff0c;为实现精准的点数控制提供了…

5 分布式ID

这里讲一个比较常用的分布式防重复的ID生成策略&#xff0c;雪花算法 一个用户体量比较大的分布式系统必然伴随着分表分库&#xff0c;分机房部署&#xff0c;单体的部署方式肯定是承载不了这么大的体量。 雪花算法的结构说明 如下图所示: 雪花算法组成 从上图我们可以看…