java timsort_TimSort排序算法及一个问题分析

TimSort排序算法及一个问题分析摘要

排序算法简析代码入口

排序算法获取两个有序数组A和B

找到待归并区间

准备操作

归并操作

TimSort的优化归并操作

问题解析问题解析

问题原因

解决方案

参考

摘要

简单介绍了传统归并排序算法,以及Java API提供的TimSort优化后的归并排序算法。

并且分析了代码中出现的一个问题原因与解决方案。

敬请忽略文中的灵魂画风。

排序算法简析

代码入口

Collections.sort、List.sort、Arrays.sort方法是逐级调用的关系,最终的底层是Arrays.sort方法,其代码如下(这几个方法的javadoc都蔚然可观,大家不妨看一看):

publicstaticvoidsort(T[] a, Comparator<?superT> c) {

if(c ==null) {

sort(a);

}else{

if(LegacyMergeSort.userRequested)

legacyMergeSort(a, c);

else

TimSort.sort(a,0, a.length, c,null,0,0);

}

}

其中的legacyMergeSort(a,c)方法已经被标记废弃;使用的主要是TimSort.sort()方法。这个方法是一个优化版的归并排序。

排序算法

这里简单描述一下核心算法,略过一些细节。

这里的描述出自个人理解(看代码和网上搜到的综合理解),可能与实际情况有出入。欢迎指正。

获取两个有序数组A和B

传统的归并排序中,A和B都是从“一个元素”开始的;“一个元素”天然有序。TimSort会通过插入排序等方法,先构建一些小的有序数组,以提高一点性能。

另外,在TimSort中,A和B都是入参a[]中的一个片段。这样可以节省一些内存空间。

e17caf89350b5f81c4778fdc1f517001.png

找到待归并区间

从数组A中,找到A[n-1]B[0],以及A[m]B[length-1]。

此时,待归并区间就是A[n,m]和B。

747dcdc4a33ce32e590b5b7797dd4dbc.png

准备操作

在正式开始归并之前,会做一些准备操作。包括将非待归并区间的数据移动到合适的位置上;准备一个临时数组、初始化一些指针数据等。如下图。

数组B被复制到了临时数组temp中。因为数组B的空间会被其他元素覆盖。

原数组A中最后一个元素“12”被放到了原数组B的最后一个位置上。因为这个元素比待归并区间所有元素都更大。

指针B1指向数组A'的第一个元素;C1指向A'的最后一个元素;B2指向B'的第一个元素;C2指向B'的最后一个元素。这四个指针是用来确定两个数组中待比较和待移动的数据范围的

指针D指向的位置,是下一个“已排序”元素的位置。也就是从A'和B'中找到的最大的元素,将会放到这个位置上。

02fd0709eb2366eb786540f652c82a85.png

归并操作

归并操作相对比较简单。依次比较A'[C1]和B'[C2],将较大的数值移动到D的位置上,并将D和对应的C1/C2向左移动一位。重复执行此操作,直到C1

下图是示例数据从准备操作(左上角标记0)到四次排序(左上角标记依次从1到4)的归并步骤。

f9c2826fa877e2481d856bb07095dc3c.png

TimSort的优化归并操作

TimSort在某些情况(触发条件待考)下,会对上述归并操作做一个优化。主要的优化点在于:不是一次一个元素的移动,而是尝试着一次移动多个元素。

下图是按优化后的逻辑,同样的示例数据从准备操作(左上角标记0)到完成排序的归并步骤。注意第一步和第二步每次都移动了两个元素。这里只用了5步就完成了归并;而优化前需要7步。

170c2e065e6f1a8732caf523d4ba39e4.png

问题解析

问题现象

代码中有一处排序逻辑,代码是这样的:List batchList = batchs.stream()

.sorted(new Comparator() {

@Override

public int compare(BatchData o1, BatchData o2) {

if (o1.getClass().getSimpleName()

.equals(o2.getClass().getSimpleName())) {

return o1.getId() - o2.getId();

}

return o1 instanceof BatchData4Company ? 1 : -1;

}

}).collect(Collectors.toList());

这段代码在某些特殊情况下,会引发这个问题:java.lang.IllegalArgumentException: Comparison method violates its general contract!

at java.util.TimSort.mergeHi(TimSort.java:899)

at java.util.TimSort.mergeAt(TimSort.java:516)

at java.util.TimSort.mergeForceCollapse(TimSort.java:457)

at java.util.TimSort.sort(TimSort.java:254)

at java.util.Arrays.sort(Arrays.java:1512)

问题原因

问题原因是,对某些数据来说,上述代码会导致compare(a,b)<0并且compare(b,a)<0,也就是a

例如,我们假定:a

假定输入数组a[] = {5,a,7,12,4,b,8,8},其中待归并的两个有序数组分别是{5,a,7,12}和{4,b,8,8}

假定b<7&&7>b。这样可以触发“特殊情况”,即:a和b在某一次归并操作后,会同时成为“是否移动元素”的临界条件。

这样,在“特殊情况”下,优化后的归并操作可能陷入死循环。用画图来表示是这样的。

获取两个有序数组A和B

首先,我们有两个有序数组A和B,如下图所示。

90f2709e3c15c941b2b264b8a3f3c5a1.png

找到待归并区间、做好准备操作

这样,在划分完待归并区间后,得到的结果是这样的:

0bbf77493a3775219f669491d92451e1.png

第一次归并操作:C2落在了元素b上

然后,开始第一次归并操作。由于B'[C2]>A'[C1],我们需要从C2开始,在数组B'中找到一个下标n,使得B'[n]

这里需要注意两点:首先,临界点的比较条件是B'[n]

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值