Java排序之TimSort源码学习笔记

本文详细介绍了Java中的TimSort排序算法,包括其核心原理和源码解析。TimSort是一种稳定排序,针对部分有序的数组能有效提高效率。文章通过实例解释了如何将数组划分为run分区,何时进行归并,以及为什么要满足特定的合并规则,以达到更高的效率。最后,文章还简要提及了TimSort在Java中的实现源码。
摘要由CSDN通过智能技术生成

1. TimSort核心原理

TimSort是结合了插入排序和归并排序稳定的排序算法,并做了许多优化,在实际应用中效率很高,因为大部分情况下待排序的数组都是部分有序(升序或降序)的。对于已经部分有序的数组,时间复杂度远低于nlog(n),最好可达到O(n);对于随机的数组,时间复杂度是nlog(n)。空间复杂度是O(n),最好的情况下是O(1)。

TimSort算法核心原理:

1、根据数组长度计算minrun(最小run(分区)长度)。

2、将数组按升序或者严格降序(需反转为升序)分割成一个一个run(分区),长度小于minrun的分区则使用插入排序进行扩充。

3、将分区的首元素下标及长度放入栈中,当栈顶run的长度满足 runLen[n-2] + runLen[n-1] >= runLen[n-3] 或者满足runLen[n-1] >= runLen[n-2]时(n表示栈中run的个数,下标n-1就是栈顶),使用归并排序将栈顶相邻最短的两个run进行合并,继续对剩余的数组元素进行分区

4、数组分区完成后将栈中剩余的run全部合并。

为什么要满足runLen[n-2] + runLen[n-1] >= runLen[n-3] 或者满足runLen[n-1] >= runLen[n-2]这个规则才合并呢?为什么不直接合并呢?
这个规则目的是找到栈顶较短且长度均衡的两个run分区,主要考虑归并操作的效率。从归并的代码层面讲,优先归并长度均衡的分区的循环次数比直接归并(长度极不均衡的分区进行归并)的循环次数要少很多,所以两个长度均衡的分区优先进行归并更合理高效。

下面通过图解的方式进行讲解,包括分区、归并操作, 我们假设minrun=5,换言之每个run分区的长度必须大于等于5:
说明: 栈中保存的是run分区的起始下标及长度,下标用于后续run分区的归并操作。
love jiaojiao
说明: run[0]分区是升序,并且长度不小于minrun,将run[0]的起始下标和长度压入栈中;run[1]降序反转为升序,且长度不小于minrun,将run[1]的起始下标和长度压入栈中;
压入栈后,此时runLen[1] >= runLen[0],满足合并规则,使用归并排序进行合并,合并结果如下:
love jiaojiao
说明: run[0]是经过一次合并后的分区,栈顶元素也随之调整;
紧接着run[1]是降序的,反转为升序并压入栈中,run[0]、run[1]不满足合并规则(runLen[n-2] + runLen[n-1] >= runLen[n-3] 或者runLen[n-1] >= runLen[n-2]),不合并;
紧接着升序的元素(0、5、6)个数小于minrun(5),使用插入排序将元素个数扩充至minrun,组成run[2]并压入栈中,此时栈顶元素run[0]、run[1]、run[2]也不满足合并规则(runLen[n-2] + runLen[n-1] >= runLen[n-3] 或者runLen[n-1] >= runLen[n-2]),不合并;
数组中还剩下两个元素,组成run[3],此时栈顶满足合并规则(runLen[n-1] >= runLen[n-2]),使用归并排序进行合并,合并结果如下:
love jiaojiao
说明: 数组分区完成后, 依次从栈顶对分区使用归并排序进行合并, 直到数组完全有序。

2. TimSort源码解析

Java中的排序最终都会调用java.util.Arrays类中的sort方法, 对于基本类型,使用的是双轴快速排序算法,此次不讲解;对于对象类型,使用的是TimSort算法,源码主要在java.util.TimSort类中,本次讲解的是通过方法传入Comparator比较器的情形,对于数组中元素对象实现Comparable接口比较大小的情况,请移步java.util.ComparableTimSort类中查看源码,实现方式大同小异,下面贴上java.util.TimSort类中排序源码,源码基本每行都添加了注释说明,以方便理解。

2.1 sort

   /**
     * 对给定范围排序,当可能的情况下(传入work参数),使用工作空间数组work作为临时存储。
     *
     * @param a 待排序的数组
     * @param lo 待排序数组的起始位置(包含)
     * @param hi 待排序数组的结束位置(不包含)
     * @param c  使用的比较器
     * @param work 一个工作空间数组(片)
     * @param workBase 工作空间中可用空间的起始位置
     * @param workLen 工作空间的可用空间长度
     * @since 1.8
     */
    static <T> void sort(T[] a, int lo, int hi, Comparator<? super T> c,
                         T[] work, int workBase, int workLen) {
   
        assert c != null && a != null && lo >= 0 && lo <= hi && hi <= a.length;
        //计算待排序数组的长度
        int nRemaining  = hi - lo;
        // 待排序数组长度小于2时肯定有序,直接返回
        if (nRemaining < 2)
            return;

        /**
         * 待排序数组长度小于MIN_MERGE(默认32)时,使用二分插入排序
          */
        if (nRemaining < MIN_MERGE) {
   
            /**
             * 查找从数组起始位置lo开始的最大升序或降序(降序的有反转操作,保证数据升序)的长度
             * 详见 2.2 countRunAndMakeAscending源码
             */
            int initRunLen = countRunAndMakeAscending(a, lo, hi, c);
            /**
             * 使用二分插入排序对指定未排序范围进行排序
             * 详见 2.3 binarySort源码
             */
            binarySort(a, lo, hi, lo + initRunLen, c);
            return;
        }

        /**
         * 根据待排序数组的长度计算最小分区大小(minRun),将待排序数组根据升序或降序(降序的进行反转为升序)进行分区(run),
         * 当分区(run)大小小于minRun时,使用二分插入法将run扩充至minRun大小,
         * 当一个分区完成后,将分区的起始位置及长度压入栈中,根据特定规则判断是否需要合并run,
         * 当所有分区完成后,将栈中剩余的分区合并
         */
        TimSort<T> ts = new TimSort<>(a, c, work, workBase, workLen);
        /**
         * 根据待排序数组长度计算最小run分区大小minRun
         * 如果数组大小为2的N次幂,则返回16(MIN_MERGE / 2)
         * 其他情况下,逐位向右位移(即除以2),直到找到介于16和32间的一个数
         * MIN_MERGE/2 <= minRun <= MIN_MERGE (MIN_MERGE默认32)
         */
        int minRun 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值