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

一、引言

在 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 底层奥秘的一把钥匙,开启更多高效编程的可能。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

[云原生之旅] K8s-Portforward的另类用法, 立省两个端口

前言 此方法适用于Pod不需要大量连接的情况: 有多个pod在执行任务, 偶尔需要连接其中一个pod查看进度/日志;对pod执行一个脚本/命令; 不适用于大量连接建立的情况: pod启的数据库服务;pod启的Api服务;pod启的前端服务;pod启的Oss服务; Portforward简介 Portforward就是端…

Transformer 中缩放点积注意力机制探讨:除以根号 dk 理由及其影响

Transformer 中缩放点积注意力机制的探讨 1. 引言 自2017年Transformer模型被提出以来&#xff0c;它迅速成为自然语言处理&#xff08;NLP&#xff09;领域的主流架构&#xff0c;并在各种任务中取得了卓越的表现。其核心组件之一是注意力机制&#xff0c;尤其是缩放点积注意…

Qt监控系统远程网络登录/请求设备列表/服务器查看实时流/回放视频/验证码请求

一、前言说明 这几个功能是近期定制的功能&#xff0c;也非常具有代表性&#xff0c;核心就是之前登录和设备信息都是在本地&#xff0c;存放在数据库中&#xff0c;数据库可以是本地或者远程的&#xff0c;现在需要改成通过网络API请求的方式&#xff0c;现在很多的服务器很强…

IDEA配置maven和git并如何使用maven打包和git推送到gitlab

首先找到设置 在里面输入maven然后找到点击 然后点击右边两个选项 路径选择下载的maven目录下的settings文件和新建的repository文件夹 点击apply应用 然后在搜索框里搜git点击进去 此路径为git的exe执行文件所在目录&#xff0c;选好之后点击test测试下方出现git版本号表…

迎接2025Power BI日期表创建指南:模板与最佳实践

故事背景 最近&#xff0c;我们收到了一些关于时间表更新的询问。询问的朋友发现&#xff0c;随着2025年的到来&#xff0c;2024年的日期表已不再适用。这是一个在数据分析领域常见的问题&#xff0c;每年都需要对日期表进行更新。 解决方案 鉴于创建和更新日期表是一项年度…

案例研究:UML用例图中的结账系统

在软件工程和系统分析中&#xff0c;统一建模语言&#xff08;UML&#xff09;用例图是一种强有力的工具&#xff0c;用于描述系统与其用户之间的交互。本文将通过一个具体的案例研究&#xff0c;详细解释UML用例图的关键概念&#xff0c;并说明其在设计结账系统中的应用。 用…

国产3D CAD将逐步取代国外软件

在工业软件的关键领域&#xff0c;计算机辅助设计&#xff08;CAD&#xff09;软件对于制造业的重要性不言而喻。近年来&#xff0c;国产 CAD 的发展态势迅猛&#xff0c;展现出巨大的潜力与机遇&#xff0c;正逐步改变着 CAD 市场长期由国外软件主导的格局。 国产CAD发展现状 …

Oopsie【hack the box】

Oopsie 解题流程 文件上传 首先开启机器后&#xff0c;我们先使用 nmap -sC -SV来扫描一下IP地址&#xff1a; -sC&#xff1a;使用 Nmap 的默认脚本扫描&#xff08;通常是 NSE 脚本&#xff0c;Nmap Scripting Engine&#xff09;。这个选项会自动执行一系列常见的脚本&am…

金山WPS Android面试题及参考答案

说说你所知道的所有集合&#xff1f;并阐述其内部实现。 在 Android 开发&#xff08;Java 语言基础上&#xff09;中有多种集合。 首先是 List 集合&#xff0c;主要包括 ArrayList 和 LinkedList。 ArrayList 是基于数组实现的动态数组。它的内部有一个数组来存储元素&#x…

快速导入请求到postman

1.确定请求&#xff0c;右键复制为cURL(bash) 2.postman菜单栏Import-Raw text&#xff0c;粘贴复制的内容保存&#xff0c;请求添加成功

Mybatis原理简介

看到Mybatis的框架图&#xff0c;可以清晰的看到Mybatis的整体核心对象&#xff0c;我更喜欢用自己的图来表达Mybatis的整个的执行流程。如下图所示&#xff1a; 原理详解&#xff1a; MyBatis应用程序根据XML配置文件创建SqlSessionFactory&#xff0c;SqlSessionFactory在根…

【redis初阶】初识Redis

目录 一、初识Redis 二、盛赞 Redis 三、Redis 特性 3.1 速度快 ​编辑3.2 基于键值对的数据结构服务器 3.3 丰富的功能 3.4 简单稳定 &#x1f436; 3.6 持久化&#xff08;Persistence&#xff09; 3.7 主从复制&#xff08;Replication&#xff09; 3.8 高可用&#xff08;H…

【云商城】高性能门户网构建

第3章 高性能门户网构建 网站门户就是首页 1.OpenResty 百万并发站点架构 ​ 1).OpenResty 特性介绍 ​ 2).搭建OpenResty ​ 3).Web站点动静分离方案剖析 2.Lua语法学习 ​ 1).Lua基本语法 3.多级缓存架构实战 ​ 1).多级缓存架构分析 用户请求网站&#xff0c;最开始…

【GESP】C++三级考试大纲知识点梳理, (1)二进制数据编码

GESP C三级官方考试大纲中&#xff0c;共有8条考点&#xff0c;本文针对C&#xff08;1&#xff09;号知识点进行总结梳理。 &#xff08;1&#xff09;了解二进制数据编码:原码、反码、补码。 全文详见&#xff1a;https://www.coderli.com/gesp-3-exam-syllabus-data-encodin…

B+树的原理及实现

文章目录 B树的原理及实现一、引言二、B树的特性1、结构特点2、节点类型3、阶数 三、B树的Java实现1、节点实现2、B树操作2.1、搜索2.2、插入2.3、删除2.4、遍历 3、B树的Java实现示例 四、总结 B树的原理及实现 一、引言 B树是一种基于B树的树形数据结构&#xff0c;它在数据…

毕业项目推荐:基于yolov8/yolov5/yolo11的动物检测识别系统(python+卷积神经网络)

文章目录 概要一、整体资源介绍技术要点功能展示&#xff1a;功能1 支持单张图片识别功能2 支持遍历文件夹识别功能3 支持识别视频文件功能4 支持摄像头识别功能5 支持结果文件导出&#xff08;xls格式&#xff09;功能6 支持切换检测到的目标查看 二、数据集三、算法介绍1. YO…

用Kimi做研究:准实验设计的智能解决方案

目录 1.研究策略设计 2.过程框架设计 3.背景变量 4.细节设计 准实验设计是一种介于实验与观察研究之间的研究方法&#xff0c;准实验设计是在无法完全控制实验条件的情况下进行因果关系的探索。与传统实验设计相比&#xff0c;准实验设计不具备随机分配实验对象到各处理组的…

【前端】【HTML】入门基础知识

参考视频&#xff1a;【狂神说Java】HTML5完整教学通俗易懂_哔哩哔哩_bilibili 一、基本结构 二、基本标签 <h1>&#xff1a;一级标题&#xff0c;通常用于页面的主标题&#xff0c;字体较大且醒目。 <h2>&#xff1a;二级标题&#xff0c;用于副标题或主要章节标…

DVT:消除视觉变换器中的噪声伪影

人工智能咨询培训老师叶梓 转载标明出处 近年来&#xff0c;视觉变换器&#xff08;Vision Transformers&#xff0c;简称ViTs&#xff09;在多种视觉任务中取得了卓越的性能&#xff0c;成为现代视觉基础模型的主流架构之一。然而&#xff0c;这些模型在特征图中存在一种网格…

OpenCV的双边滤波函数

OpenCV的双边滤波函数cv2.bilateralFilter是一种用于图像处理的强大工具&#xff0c;它能够在去除噪声的同时保持边缘的清晰度。以下是对该函数的详细说明&#xff1a; 一、函数原型 python cv2.bilateralFilter(src, d, sigmaColor, sigmaSpace[, dst[, borderType]])二、参…