Java - 源码之 Arrays 内部排序 TimSort 实现

简介: Java - 源码之 Arrays 内部排序 TimSort 实现

Timsort是结合了合并排序(merge sort)插入排序insertion sort)而得出的排序算法,它在现实中有很好的效率。Tim Peters在2002年设计了该算法并在Python中使用(TimSort 是 Python 中 list.sort 的默认实现)。该算法找到数据中已经排好序的块-分区,每一个分区叫一个run,然后按规则合并这些run。Pyhton自从2.3版以来一直采用Timsort算法排序,JDK 1.7开始也采用Timsort算法对数组排序。

在Arrays工作类里有sort()方法可以用来排序,jdk对所有基本类型设置设置了不同入参sort方法进行支持。

image.png

从源码上看,基本类型的排序都是使用了了DualPivotQuicksort的排序方法(我看的是jdk8,)。DualPivotQuicksort是快排的一种优化,具体在这里不展开了。


当参数类型为对象数组时,在原来的版本使用的归并排序(以后将会删除 ),现在使用的timSort。

publicstaticvoidsort(Object[] a) {
if (LegacyMergeSort.userRequested)
legacyMergeSort(a);
elseComparableTimSort.sort(a);
}
// 以后会抛弃,也不展开了,大家可以自己去看下归并排序/** To be removed in a future release. */privatestaticvoidlegacyMergeSort(Object[] a) {
Object[] aux=a.clone();
mergeSort(aux, a, 0, a.length, 0);
}

所以排序主要用了 ComparableTimSort.sort(Object[] a)。分为下面几个主要步骤:

数组个数小于32的情况

判断数组的大小,小于32使用二分插入排序

staticvoidsort(Object[] a, intlo, inthi) {
// 检查lo,hi的的准确性rangeCheck(a.length, lo, hi);
intnRemaining=hi-lo;
// 当长度为0或1时永远都是已经排序状态if (nRemaining<2)
return;
// 数组小的时候if (nRemaining<MIN_MERGE) {
//找出连续升序的最大个数intinitRunLen=countRunAndMakeAscending(a, lo, hi);
//二分插入排序binarySort(a, lo, hi, lo+initRunLen);
return;
    }
// 数组大于32的时   ......

找出最大的递增或者递减的个数,如果递减,则此段数组严格反一下方向

privatestaticintcountRunAndMakeAscending(Object[] a, intlo, inthi) {
intrunHi=lo+1;
if (runHi==hi)
return1;
// Find end of run, and reverse range if descendingif (((Comparable) a[runHi++]).compareTo(a[lo]) <0) { // 递减while (runHi<hi&& ((Comparable) a[runHi]).compareTo(a[runHi-1]) <0)
runHi++;
// 调整顺序reverseRange(a, lo, runHi);
    } else { // 递增                              while (runHi<hi&& ((Comparable) a[runHi]).compareTo(a[runHi-1]) >=0)
runHi++;
    }
returnrunHi-lo;
}

使用在使用二分查找位置,进行插入排序。start之前为全部递增数组,从start+1开始进行插入,插入位置使用二分法查找。最后根据移动的个数使用不同的移动方法。

privatestaticvoidbinarySort(Object[] a, intlo, inthi, intstart) {
if (start==lo)
start++;
for ( ; start<hi; start++) {
Comparable<Object>pivot= (Comparable) a[start];
intleft=lo;
intright=start;
while (left<right) {
intmid= (left+right) >>>1;
if (pivot.compareTo(a[mid]) <0)
right=mid;
elseleft=mid+1;
        }
intn=start-left;  // 要移动的个数// 移动的方法switch (n) {
case2:  a[left+2] =a[left+1];
case1:  a[left+1] =a[left];
break;
// native复制数组方法default: System.arraycopy(a, left, a, left+1, n);
        }
a[left] =pivot;
    }
}

数组个数大于32的情况

数组大于32时, 先算出一个合适的大小,在将输入按其升序和降序特点进行了分区。排序的输入的单位不是一个个单独的数字,而是一个个的块-分区。其中每一个分区叫一个run。针对这些 run 序列,每次拿一个run出来按规则进行合并。每次合并会将两个run合并成一个 run。合并的结果保存到栈中。合并直到消耗掉所有的run,这时将栈上剩余的 run合并到只剩一个 run 为止。这时这个仅剩的 run 便是排好序的结果。

staticvoidsort(Object[] a, intlo, inthi) {
// 小于32    ......
// 大于32的情况ComparableTimSortts=newComparableTimSort(a);
// 计算出run的长度intminRun=minRunLength(nRemaining);
do {
// 找出连续升序的最大个数intrunLen=countRunAndMakeAscending(a, lo, hi);
// 如果run长度小于规定的minRun长度,先进行二分插入排序if (runLen<minRun) {
intforce=nRemaining<=minRun?nRemaining : minRun;
binarySort(a, lo, lo+force, lo+runLen);
runLen=force;
        }
// Push run onto pending-run stack, and maybe mergets.pushRun(lo, runLen);
// 进行归并ts.mergeCollapse();
lo+=runLen;
nRemaining-=runLen;
    } while (nRemaining!=0);
// 归并所有的runts.mergeForceCollapse();
}

计算出run的最小的长度minRun

a)如果数组大小为2的N次幂,则返回16(MIN_MERGE / 2)

b)其他情况下,逐位向右位移(即除以2),直到找到介于16和32间的一个数

privatestaticintminRunLength(intn) {
// Becomes 1 if any 1 bits are shifted offintr=0;      
while (n>=MIN_MERGE) {
r|= (n&1);
n>>=1;
    }
returnn+r;
}

求最小递增的长度,如果长度小于minRun,使用插入排序补充到minRun的个数,操作和小于32的个数是一样。

用stack记录每个run的长度,当下面的条件其中一个成立时归并,直到数量不变

  • runLen[i - 3] > runLen[i - 2] + runLen[i - 1]
  • runLen[i - 2] > runLen[i - 1]
privatevoidmergeCollapse() {
while (stackSize>1) {
intn=stackSize-2;
if (n>0&&runLen[n-1] <=runLen[n] +runLen[n+1]) {
if (runLen[n-1] <runLen[n+1])
n--;
// 具体的归并操作mergeAt(n);
        } elseif (runLen[n] <=runLen[n+1]) {
mergeAt(n);
        } else {
break; // Invariant is established        }
    }
}

关于归并方法和对一般的归并排序做出了简单的优化。假设两个 run 是 run1,run2 ,先用 gallopRight在 run1 里使用 binarySearch 查找run2 首元素 的位置k, 那么 run1 中 k 前面的元素就是合并后最小的那些元素。然后,在run2 中查找run1 尾元素 的位置 len2 ,那么run2 中 len2 后面的那些元素就是合并后最大的那些元素。最后,根据len1 与len2 大小,调用mergeLo 或者 mergeHi 将剩余元素合并。

privatevoidmergeAt(inti) {
intbase1=runBase[i];
intlen1=runLen[i];
intbase2=runBase[i+1];
intlen2=runLen[i+1];
runLen[i] =len1+len2;
if (i==stackSize-3) {
runBase[i+1] =runBase[i+2];
runLen[i+1] =runLen[i+2];
    }
stackSize--;
intk=gallopRight((Comparable<Object>) a[base2], a, base1, len1, 0);
assertk>=0;
base1+=k;
len1-=k;
if (len1==0)
return;
len2=gallopLeft((Comparable<Object>) a[base1+len1-1], a,
base2, len2, len2-1);
assertlen2>=0;
if (len2==0)
return;
if (len1<=len2)
mergeLo(base1, len1, base2, len2);
elsemergeHi(base1, len1, base2, len2);
}

最后归并还有没有归并的run,知道run的数量为1

例子

为了演示方便,我将TimSort中的minRun直接设置为2,否则我不能用很小的数组演示。。。同时把MIN_MERGE也改成2(默认为32),这样避免直接进入二分插入排序。

初始数组为[7,5,1,2,6,8,10,12,4,3,9,11,13,15,16,14]


寻找第一个连续的降序或升序序列:[1,5,7] [2,6,8,10,12,4,3,9,11,13,15,16,14]


stackSize=1,所以不合并,继续找第二个run


找到一个递减序列,调整次序:[1,5,7] [2,6,8,10,12] [4,3,9,11,13,15,16,14]


因为runLen[0]<=runLen[1]所以归并

1) gallopRight:寻找run1的第一个元素应当插入run0中哪个位置(”2”应当插入”1”之后),然后就可以忽略之前run0的元素(都比run1的第一个元素小)

2) gallopLeft:寻找run0的最后一个元素应当插入run1中哪个位置(”7”应当插入”8”之前),然后就可以忽略之后run1的元素(都比run0的最后一个元素大)

这样需要排序的元素就仅剩下[5,7] [2,6],然后进行mergeLow 完成之后的结果: [1,2,5,6,7,8,10,12] [4,3,9,11,13,15,16,14]


寻找连续的降序或升序序列[1,2,5,6,7,8,10,12] [3,4] [9,11,13,15,16,14]


不进行归并排序,因为runLen[0]>runLen[1]


寻找连续的降序或升序序列:[1,2,5,6,7,8,10,12] [3,4] [9,11,13,15,16] [14]


因为runLen[1]<=runLen[2],所以需要归并


使用gallopRight,发现为正常顺序。得[1,2,5,6,7,8,10,12] [3,4,9,11,13,15,16] [14]


最后只剩下[14]这个元素:[1,2,5,6,7,8,10,12] [3,4,9,11,13,15,16] [14]


因为runLen[0]<=runLen[1]+runLen[2]所以合并。因为runLen[0]>runLen[2],所以将run1和run2先合并。(否则将run0和run1先合并)

完成之后的结果: [1,2,5,6,7,8,10,12] [3,4,9,11,13,14,15,16]


完成之后的结果:[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16]

性能分析

本质上 Timsort 是一个经过大量优化的归并排序,而归并排序已经到达了最坏情况下,比较排序算法时间复杂度的下界,所以在最坏的情况下,Timsort 时间复杂度为 O(nlogn) O(nlogn)O(nlogn)。在最佳情况下,即输入已经排好序,它则以线性时间运行O(n) O(n)O(n)。可以看出Timsort是目前最好的排序方式。


目录
相关文章
|
前端开发 Java 关系型数据库
基于Java+Springboot+Vue开发的鲜花商城管理系统源码+运行
基于Java+Springboot+Vue开发的鲜花商城管理系统(前后端分离),这是一项为大学生课程设计作业而开发的项目。该系统旨在帮助大学生学习并掌握Java编程技能,同时锻炼他们的项目设计与开发能力。通过学习基于Java的鲜花商城管理系统项目,大学生可以在实践中学习和提升自己的能力,为以后的职业发展打下坚实基础。技术学习共同进步
862 7
|
人工智能 安全 Java
智慧工地源码,Java语言开发,微服务架构,支持分布式和集群部署,多端覆盖
智慧工地是“互联网+建筑工地”的创新模式,基于物联网、移动互联网、BIM、大数据、人工智能等技术,实现对施工现场人员、设备、材料、安全等环节的智能化管理。其解决方案涵盖数据大屏、移动APP和PC管理端,采用高性能Java微服务架构,支持分布式与集群部署,结合Redis、消息队列等技术确保系统稳定高效。通过大数据驱动决策、物联网实时监测预警及AI智能视频监控,消除数据孤岛,提升项目可控性与安全性。智慧工地提供专家级远程管理服务,助力施工质量和安全管理升级,同时依托可扩展平台、多端应用和丰富设备接口,满足多样化需求,推动建筑行业数字化转型。
468 5
|
消息中间件 算法 安全
JUC并发—1.Java集合包底层源码剖析
本文主要对JDK中的集合包源码进行了剖析。
|
10月前
|
存储 小程序 Java
热门小程序源码合集:微信抖音小程序源码支持PHP/Java/uni-app完整项目实践指南
小程序已成为企业获客与开发者创业的重要载体。本文详解PHP、Java、uni-app三大技术栈在电商、工具、服务类小程序中的源码应用,提供从开发到部署的全流程指南,并分享选型避坑与商业化落地策略,助力开发者高效构建稳定可扩展项目。
|
监控 Java API
Java语言按文件创建日期排序及获取最新文件的技术
这段代码实现了文件创建时间的读取、文件列表的获取与排序以及获取最新文件的需求。它具备良好的效率和可读性,对于绝大多数处理文件属性相关的需求来说足够健壮。在实际应用中,根据具体情况,可能还需要进一步处理如访问权限不足、文件系统不支持某些属性等边界情况。
510 14
|
JavaScript Java 关系型数据库
家政系统源码,java版本
这是一款基于SpringBoot后端框架、MySQL数据库及Uniapp移动端开发的家政预约上门服务系统。
456 6
家政系统源码,java版本
|
供应链 JavaScript 前端开发
Java基于SaaS模式多租户ERP系统源码
ERP,全称 Enterprise Resource Planning 即企业资源计划。是一种集成化的管理软件系统,它通过信息技术手段,将企业的各个业务流程和资源管理进行整合,以提高企业的运营效率和管理水平,它是一种先进的企业管理理念和信息化管理系统。 适用于小微企业的 SaaS模式多租户ERP管理系统, 采用最新的技术栈开发, 让企业简单上云。专注于小微企业的应用需求,如企业基本的进销存、询价,报价, 采购、销售、MRP生产制造、品质管理、仓库库存管理、财务应收付款, OA办公单据、CRM等。
992 23
|
Java
【源码】【Java并发】【ConcurrentHashMap】适合中学体质的ConcurrentHashMap
本文深入解析了ConcurrentHashMap的实现原理,涵盖JDK 7与JDK 8的区别、静态代码块、构造方法、put/get/remove核心方法等。JDK 8通过Node数组+链表/红黑树结构优化并发性能,采用CAS和synchronized实现高效锁机制。文章还详细讲解了hash计算、表初始化、扩容协助及计数更新等关键环节,帮助读者全面掌握ConcurrentHashMap的工作机制。
388 6
【源码】【Java并发】【ConcurrentHashMap】适合中学体质的ConcurrentHashMap
|
存储 安全 Java
Java 集合面试题从数据结构到 HashMap 源码剖析详解及长尾考点梳理
本文深入解析Java集合框架,涵盖基础概念、常见集合类型及HashMap的底层数据结构与源码实现。从Collection、Map到Iterator接口,逐一剖析其特性与应用场景。重点解读HashMap在JDK1.7与1.8中的数据结构演变,包括数组+链表+红黑树优化,以及put方法和扩容机制的实现细节。结合订单管理与用户权限管理等实际案例,展示集合框架的应用价值,助你全面掌握相关知识,轻松应对面试与开发需求。
597 3
|
Java 关系型数据库 MySQL
Java汽车租赁系统源码(含数据库脚本)
Java汽车租赁系统源码(含数据库脚本)
715 4