当前位置:

首页 > 编程开发 > Java怎么实现排序算法Timsort

Java怎么实现排序算法Timsort

背景Timsort是一个混合、稳定的排序算法,简单来说就是归并排序和二分插入排序算法的混合体,号称世界上最好的排序算法。Timsort一直是Python的标准排序算法。JavaSE7后添加了TimsortAPI,我们从Arrays.sort可以看出它已经是非原始类型数组的默认排序算法了。所以不管是进阶编程学习还是面试,理解Timsort是比较重要。//Listsort()defaultvoidsort(Comparator

本文目录

    背景

    Timsort 是一个混合、稳定的排序算法,简单来说就是归并排序和二分插入排序算法的混合体,号称世界上最好的排序算法。Timsort一直是 Python 的标准排序算法。Java SE 7 后添加了Timsort API ,我们从Arrays.sort可以看出它已经是非原始类型数组的默认排序算法了。所以不管是进阶编程学习还是面试,理解 Timsort 是比较重要。

    // List sort()    
    default void sort(Comparator c) {
            Object[] a = this.toArray();
                  //数组排序
            Arrays.sort(a, (Comparator) c);
                  ...
        }
    
    // Arrays.sort
        public static  void sort(T[] a, Comparator c) {
            if (c == null) {
                sort(a);
            } else {
                  // 废弃版本
                if (LegacyMergeSort.userRequested)
                    legacyMergeSort(a, c);
                else
                    TimSort.sort(a, 0, a.length, c, null, 0, 0);
            }
        }
    
        public static void sort(Object[] a) {
            if (LegacyMergeSort.userRequested)
                legacyMergeSort(a);
            else
                ComparableTimSort.sort(a, 0, a.length, null, 0, 0);
        }

    前置知识

    理解 Timsort 需要先回顾下面的知识。

    指数搜索

    指数搜索,也被称为加倍搜索,是一种用于在大型数组中搜索元素而创建的算法。它是一个两步走的过程。首先,该算法试图找到目标元素存在的范围 (L,R),然后在这个范围内使用二叉搜索来寻找目标的准确位置。时间复杂度为O(lgn)。该搜索算法在大量有序数组中比较有效。

    二分插入排序

    插入排序算法很简单,大体过程是从第二个元素开始,依次向前移动交换元素直到找到合适的位置。

    Java怎么实现排序算法Timsort

    插入排序最优时间复杂度也要O(n) ,我们可以使用二分查找来减少插入时元素的比较次数,将时间复杂度降为logn。但是注意,二分查找插入排序仍然需要移动相同数量的元素,但是复制数组的时间消耗低于逐一互换操作。

    特点:二分插入排序主要优点是在小数据集场景下排序效率很高。

    public static int[] sort(int[] arr) throws Exception {
            // 开始遍历第一个元素后的所有元素
            for (int i = 1; i < arr.length; i++) {
                // 需要插入的元素
                int tmp = arr[i];
                // 从已排序最后一个元素开始,如果当前元素比插入元素大,就往后移动
                int j = i;
                while (j > 0 && tmp < arr[j - 1]) {
                    arr[j] = arr[j - 1];
                    j--;
                }
                // 将元素插入
                if (j != i) {
                    arr[j] = tmp;
                }
            }
            return arr;
        }
    
        public static int[] binarySort(int[] arr) throws Exception {
            for (int i = 1; i < arr.length; i++) {
                // 需要插入的元素
                int tmp = arr[i];
                // 通过二分查找直接找到插入位置
                int j = Math.abs(Arrays.binarySearch(arr, 0, i, tmp) + 1);
                // 找到插入位置后,通过数组复制向后移动,腾出元素位置
                System.arraycopy(arr, j, arr, j + 1, i - j);
                // 将元素插入
                arr[j] = tmp;
            }
            return arr;
        }

    归并排序

    归并排序是利用分治策略的算法,包含两个主要的操作:分割与合并。大体过程是,通过递归将数组不断分成两半,一直到无法再分割(也就是数组为空或只剩一个元素),然后进行合并排序。简单来说合并操作就是不断取两个较小的排序数组然后将它们组合成一个更大的数组。

    特点:归并排序主要为大数据集场景设计的排序算法。

    Java怎么实现排序算法Timsort

    public static void mergeSortRecursive(int[] arr, int[] result, int start, int end) {
            // 跳出递归
            if (start >= end) {
                return;
            }
            // 待分割的数组长度
            int len = end - start;
            int mid = (len >> 1) + start;
            int left = start; // 左子数组开始索引
            int right = mid + 1; // 右子数组开始索引
            // 递归切割左子数组,直到只有一个元素
            mergeSortRecursive(arr, result, left, mid);
            // 递归切割右子数组,直到只有一个元素
            mergeSortRecursive(arr, result, right, end);
            int k = start;
            while (left <= mid && right <= end) {
                result[k++] = arr[left] < arr[right] ? arr[left++] : arr[right++];
            }
            while (left <= mid) {
                result[k++] = arr[left++];
            }
            while (right <= end) {
                result[k++] = arr[right++];
            }
            for (k = start; k <= end; k++) {
                arr[k] = result[k];
            }
        }
    
        public static int[] merge_sort(int[] arr) {
            int len = arr.length;
            int[] result = new int[len];
            mergeSortRecursive(arr, result, 0, len - 1);
            return arr;
        }

    Timsort 执行过程

    算法大体过程,如果数组长度小于指定阀值(MIN_MERGE)直接使用二分插入算法完成排序,否则执行下面步骤:

    • 先从数组左边开始,执行升序运行得到一个子序列。

    • 将这个子序列放入运行堆栈里,等待执行合并。

    • 检查运行堆栈里的子序列,如果满足合并条件则执行合并。

    • 重复第一个步骤,执行下一个升序运行。

    升序运行

    升序运行就是从数组查找一段连续递增(升序)或递减(降序)子序列的过程,如果子序列为降序则将它反转为升序,也可以将这个过程简称为 run。比如数组 [2,3,6,4,9,30],可以查找到三个子序列,[2,3,6]、[4,9]、[30],或说3个 run。

    几个关键阀值

    MIN_MERGE

    这是个常数值,可以简单理解为执行归并的最小阀值,如果整个数组长度小于它,就没必要执行那么复杂的排序,直接二分插入就行了。在 Tim Peter 的 C 实现中为 64,但实际经验中设置为 32 效果更好,所以 java 里面此值为 32。

    降序反转时为保证稳定性,相同元素不会被颠倒。

    minrun

    在合并序列的时候,如果 run 数量等于或者略小于 2 的幂次方的时候,合并效率最高;如果略大于 2 的幂次方,效率就会显著降低。所以为了提高合并效率,需要尽量控制每个 run 的长度,通过定义一个 minrun 来表示每个 run 的最小长度,如果长度太短,就用二分插入排序把 run 后面的元素插入到前面的 run 里面。

    一般在执行排序算法之前,会先计算出这个 minrun(它是根据数据的特点来进行自我调整),minrun 会从32到64选择一个数字,因此数据的大小除以 minrun 等于或略小于 2 的幂次方。比如长度是 65 ,那么 minrun 的值就是 33;如果长度是 165,minrun 就是 42。

    看下 Java 里面的实现,如果数据长度(n) < MIN_MERGE,则返回数据长度。如果数据长度恰好是 2 的幂次方,则返回MIN_MERGE/2
    也就是16,否则返回一个MIN_MERGE/2 <= k <= MIN_MERGE范围的值k,这样可以使的 n/k 接近但严格小于 2 的幂次方。

    private static int minRunLength(int n) {
            assert n >= 0;
            int r = 0;      // 如果低位任何一位是1,就会变成1
            while (n >= MIN_MERGE) {
                r |= (n & 1);
                n >>= 1;
            }
            return n + r;
        }

    MIN_GALLOP

    MIN_GALLOP 是为了优化合并的过程设定的一个阈值,控制进入 GALLOP 模式中, GALLOP 模式放在后面讲。

    下面是 Timsort 执行流程图

    Java怎么实现排序算法Timsort

    运行合并

    当栈里面的 run 满足合并条件时,它就将栈里面相邻的两个run 进行合并。

    合并条件

    Timsort 为了执行平衡合并(让合并的 run 大小尽可能相同),制定了一个合并规则,对于在栈顶的三个run,分别用X、Y 和 Z 表示他们的长度,其中 X 在栈顶,它们必须始终维持一下的两个规则:

    Java怎么实现排序算法Timsort

    一旦有其中的一个条件不被满足,则将 Y 与 X 或 Z 中的较小者合并生成新的 run,并再次检查栈顶是否仍然满足条件。如果不满足则会继续进行合并,直至栈顶的三个元素都满足这两个条件,如果只剩两个run,则满足 Y > X 即可。

    如下下图例子

    • 当 Z <= Y+X ,将 X+Y 合并,此时只剩下两个run。

    • 检测 Y < X ,执行合并,此时只剩下 X,则退出合并检测。

    Java怎么实现排序算法Timsort

    我们看下 Java 里面的合并实现

    private void mergeCollapse() {
          
           // 当存在两个以上run执行合并检查
            while (stackSize > 1) {
    
              // 表示 Y
                int n = stackSize - 2; 
               
              // Z <= Y + X 
                if (n > 0 && runLen[n-1] <= runLen[n] + runLen[n+1]) { 
    
                  // 如果 Z < X 合并Z+Y ,否则合并X+Y
                  if (runLen[n - 1] < runLen[n + 1])
                        n--;
                    
                    // 合并相邻的两个run,也就是runLen[n] 和 runLen[n+1]
                    mergeAt(n); 
                } else if (runLen[n] <= runLen[n + 1]) {
                    
                    // Y <= X 合并 Y+X
                    mergeAt(n);
                } else {
                    
                    // 满足两个条件,跳出循环
                    break; 
                }
            }
        }

    合并内存开销

    原始归并排序空间复杂度是 O(n)也就是数据大小。为了实现中间项,Timsort 进行了一次归并排序,时间开销和空间开销都比 O(n)小。

    优化是为了尽可能减少数据移动,占用更少的临时内存,先找出需要移动的元素,然后将较小序列复制到临时内存,在按最终顺序排序并填充到组合序列中。

    比如我们需要合并 X [1, 2, 3, 6, 10] 和 Y [4, 5, 7, 9, 12, 14, 17],X 中最大元素是10,我们可以通过二分查找确定,它需要插入到 Y 的第 5个位置才能保证顺序,而 Y 中最小元素是4,它需要插入到 X 中的第4个位置才能保证顺序,那么就知道了[1, 2, 3] 和 [12, 14, 17] 不需要移动,我们只需要移动 [6, 10] 和 [4, 5, 7, 9],然后只需要分配一个大小为 2 临时存储就可以了。

    合并优化

    在归并排序算法中合并两个数组需要一一比较每个元素,为了优化合并的过程,设定了一个阈值 MIN_GALLOP,当B中元素向A合并时,如果A中连续 MIN_GALLOP 个元素比B中某一个元素要小,那么就进入GALLOP模式。

    根据基准测试,比如当A中连续7个以上元素比B中某一元素小时切入该模式效果才好,所以初始值为7。

    当进入GALLOP模式后,搜索算法变为指数搜索,分为两个步骤,比如想确定 A 中元素x在 B 中确定的位置

    • 首先在 B 中找到合适的索引区间(2k−1,2k+1−1) 使得 x 元素在这个范围内;

    • 然后在第一步找到的范围内通过二分搜索来找到对应的位置。

    只有当一次运行的初始元素不是另一次运行的前七个元素之一时,驰骋才是有益的。这意味着初始阈值为 7。

    本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系bd@zhengruan.com
    作者最新文章
    编程开发 Java
    相关文章 更多
    Java单元测试入门:使用JUnit 5编写第一个测试用例
    Java单元测试入门:使用JUnit 5编写第一个测试用例

    面向初学者的JUnit 5实用入门指南。从测试概念、Maven依赖配置,到手把手编写首个单元测试用例及断言验证,助你掌握Java单元测试的核心流程。

    Java变量未初始化报错解析:局部变量与成员变量默认值差异
    Java变量未初始化报错解析:局部变量与成员变量默认值差异

    深入解析Java中局部变量未初始化编译报错与成员变量自动赋予默认值的机制差异,涵盖JVM内存分配、编译期确定性赋值检查与实际排查修复方案。

    Java获取当前时间并格式化输出:DateTimeFormatter与SimpleDateFormat
    Java获取当前时间并格式化输出:DateTimeFormatter与SimpleDateFormat

    详细讲解在Java中获取当前系统时间并进行格式化输出的两种核心方案:基于Java 8全新日期时间API的DateTimeFormatter与传统遗留的SimpleDateFormat,包含完整代码示例、线程安全机制对比及生产选型建议。

    Java中final与static final常量的定义及初始化规则
    Java中final与static final常量的定义及初始化规则

    深入解析Java中final变量与static final常量的定义、内存语义、初始化时机及合法赋值路径,涵盖编译期常量优化与实战避坑指南。

    Java抽象方法与接口方法的区别及选型建议
    Java抽象方法与接口方法的区别及选型建议

    深入对比Java中抽象类抽象方法与接口方法的语法演进、设计意图及运行期差异,并提供清晰的架构选型决策流程与实战范例。

    Java实现HTTP请求与JSON解析的完整代码示例
    Java实现HTTP请求与JSON解析的完整代码示例

    本文提供Java标准库HttpClient发起HTTP GET/POST请求并结合Jackson进行JSON数据反序列化与序列化的完整代码示例与工程实践指南。

    Java递归出现StackOverflowError的原因分析及栈空间优化方案
    Java递归出现StackOverflowError的原因分析及栈空间优化方案

    详细分析Java递归调用出现StackOverflowError的原因,结合代码说明线程栈、栈帧和递归深度之间的关系,并介绍终止条件排查、迭代改写、减少栈帧开销以及通过-Xss调整JVM栈空间的方法。

    Java空指针异常(NPE)的代码排查与修复实例
    Java空指针异常(NPE)的代码排查与修复实例

    针对Java中常见的NullPointerException(NPE),本文从堆栈日志定位、典型代码触发场景、具体修复代码对比到防御体系进行系统讲解,提供实用且易上手的排查与修复方案。

    selenium上传文件教程本地文件路径设置方法
    selenium上传文件教程本地文件路径设置方法

    深入解析Selenium处理文件上传的技术细节,涵盖标准HTML控件的路径直接注入法与非标准控件的系统级模拟方案,提供Windows与Linux环境下的路径设置最佳实践及常见异常排查。

    大数据开发语言选型:Java对比Scala与Python的优缺点及适用场景
    大数据开发语言选型:Java对比Scala与Python的优缺点及适用场景

    针对大数据工程场景,系统对比Java、Scala与Python在执行性能、生态结合、开发效率和维护成本上的核心差异,给出清晰的架构选型依据与落地方案。

    查看更多
    精品专题 更多
    装机必备
    装机必备

    正软商城装机必备专区,精选办公、浏览器、安全防护、影音播放、压缩解压、设计创作和系统工具等电脑常用正版软件,帮助用户快速完成新电脑软件配置。

    Windows
    Windows

    正软商城Windows软件专区,汇集适用于Windows电脑的办公、设计、安全防护、影音播放、开发工具和系统优化软件,提供软件介绍、系统要求、正版授权及购买下载服务。

    PDF教程
    PDF教程

    正软商城PDF教程频道提供PDF编辑、转换、合并、拆分、压缩及格式处理方法,同时介绍常用PDF软件和工具的使用技巧。

    Mac软件 更多
    讯飞智作 网页版
    讯飞智作 网页版
    Windows/ macOS/ Linux

    讯飞智作是一款基于语音合成技术、虚拟人驱动技术与数字人形象声音构建能力的 AI 数字人音视频创作平台。

    火山引擎 网页版
    火山引擎 网页版
    Windows/ macOS/ Linux

    火山引擎是字节跳动旗下企业级云与AI服务平台。聚焦大模型核心能力,依托字节自研豆包多模态大模型,凭借火山方舟一站式平台,提供模型精调、推理部署、智能应用搭建等全链路服务,结合高性能AI算力,助力各行业快速落地大模型数字化与智能化应用。

    Aperty macOS版
    Aperty macOS版
    macOS

    Aperty 是 Skylum 推出的 AI 人像修图软件,支持自然肤质修饰、瑕疵处理、面部与身体调整、预设及批量同步。

    WINDOWS 更多
    讯飞智作 网页版
    讯飞智作 网页版
    Windows/ macOS/ Linux

    讯飞智作是一款基于语音合成技术、虚拟人驱动技术与数字人形象声音构建能力的 AI 数字人音视频创作平台。

    火山引擎 网页版
    火山引擎 网页版
    Windows/ macOS/ Linux

    火山引擎是字节跳动旗下企业级云与AI服务平台。聚焦大模型核心能力,依托字节自研豆包多模态大模型,凭借火山方舟一站式平台,提供模型精调、推理部署、智能应用搭建等全链路服务,结合高性能AI算力,助力各行业快速落地大模型数字化与智能化应用。

    PixTV 网页版
    PixTV 网页版
    Windows

    PixTV 是一款在浏览器中使用的 AI 视觉内容创作平台,提供图片、视频、音频、文字生成、AI 分镜、局部重绘、提示词反推和无限画布等工具,适合短剧、漫剧、广告及社交媒体内容制作。