Comparison method violates its general contract! 神奇的报错

2024-06-21 20:20

本文主要是介绍Comparison method violates its general contract! 神奇的报错,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

发生情况

定位到问题代码如下(脱敏处理过后),意思是集合排序,如果第一个元素大于第二个元素,比较结果返回1,否则返回-1,这里粗略的认为小于和等于是一样的结果

List<Integer> list = Arrays.asList(2213, 2214, 2235, 2211, 228, 2233, 2215, 2229, 2212, 0, 2245, 2220, 225,2237, 2241, 229,2221, 227, 2236, 226, 2246, 2222, 2247, 2232, 2240, 225, 2218, 2227, 2248, 2216, 2217, 2223);
list.sort((o1, o2) -> o1 > o2 ? 1 : -1);

之前没问题啊,对 这个问题不是百分之百复现的,只需要将上述集合删除一些元素,报错就不会发生了看了一些人的文章,基本上意思是Collections.sort()在JDK6中使用的时MergeSort排序,在JDK1.7之后默认排序方式做了修改,使用TimeSort排序算法来排序,查看了一下Arrays.java源码,果然如此

public static <T> void sort(T[] a, Comparator<? super T> c) {if (c == null) {sort(a);} else {if (LegacyMergeSort.userRequested)legacyMergeSort(a, c);elseTimSort.sort(a, 0, a.length, c, null, 0, 0);}}
那么TimSort排序有啥说法呢?此排序算法比老版本的算法多了如下几个限制条件,如果不注意,排序可能会抛异常
  1. 自反性,x,y 的比较结果和 y,x 的比较结果相反,即compare(x, y) = - compare(y, x)
  2. 传递性,如果compare(x, y) > 0, compare(y, z) > 0, 则必须保证compare(x, z) > 0
  3. 对称性, 如果compare(x, y) == 0, 则必须保证compare(x, z) == compare(y, z)
上面的表达式(o1, o2) -> o1 > o2 ? 1 : -1,显然不满足 自反性,例如o1=5,o2=5,那么compare(o1,o2)返回的-1,根据自反型原则compare(o2,o1)应该返回的1 ,实际上返回的确还是-1

基本解决

正确写法是排序方法要考虑返回每种情况 -1,0,1 三种
list.sort(new Comparator<Integer>() {@Overridepublic int compare(Integer integer, Integer anotherInteger) {return integer.compareTo(anotherInteger); //或者写成 return (x < y) ? -1 : ((x == y) ? 0 : 1);}
});

或者直接用stream 流方式来排序就好,我也比较喜欢这种方式

 // 升序
list=list.stream().sorted().collect(Collectors.toList());
// 降序
list=list.stream().sorted(Comparator.reverseOrder()).collect(Collectors.toList());

探索底层报错原因

简述Timsort算法:

       Timsort排序算法是一种结合了归并排序(merge sort)和插入排序(insertion sort)的高效排序算法,专为现实世界的数据排序需求而设计。TimSort 算法为了减少对升序部分的回溯和对降序部分的性能倒退,将输入按其升序和降序特点进行了分区。排序的输入的单位不是一个个单独的数字,而是一个个的块-分区。其中每一个分区叫一个run。针对这些 run 序列,每次拿一个 run 出来按规则进行合并。每次合并会将两个 run合并成一个 run。合并的结果保存到栈中。合并直到消耗掉所有的 run,这时将栈上剩余的 run合并到只剩一个 run 为止。这时这个仅剩的 run 便是排好序的结果。

基本思想:

  • 利用现实数据中通常存在的部分已排序的特点,将数组拆分成多个已排序的分区(称为“run”)和未排序的分区。
  • 对未排序的分区使用插入排序或其他方法进行排序,使其成为新的已排序分区。
  • 最后,通过归并排序的思想将这些已排序的分区合并成一个完全有序的数组。

分区方法:

  • 假定数据长度为n。
  • 如果n小于某个阈值(如32或64,具体值可能因不同编程语言实现而异),则直接对整个数组使用插入排序或其他简单排序算法。
  • 如果n大于或等于阈值,则先找到已排序的分区(run),并对这些分区进行标记。
  • 如果分区是降序的,则将其翻转为升序。

合并规则:

  • 使用一个栈来保存已排序的分区(run)。
  • 从栈顶开始,按照归并排序的合并策略,将两个相邻的分区合并成一个更大的已排序分区。
  • 合并的结果(新的已排序分区)继续压入栈中。
  • 重复这个过程,直到栈中只剩下一个分区,此时整个数组已经排序完成。

特殊优化:

  • Galloping模式:在合并过程中,使用一种称为“Galloping”的策略来快速跳过相等或接近相等的元素,从而提高合并效率。
  • 二分插入排序:对于较小的分区(如长度小于阈值的分区),使用二分插入排序算法来提高排序效率

拆分RUN

那么首先看我们要排序的例子,正好32个,所以分为两个RUN,暂时成为 ra 和rb【划分的块数最好是完整二叉树的叶子节点的数量 即二的整数次幂(2,4,8...)  并且最好每个块要大小均匀而且维持在16到32之间的小块 这样效率最高 】

分别排序

那么接下来我们要做的就是分别讲ra 和rb 进行排序:
对整个List开始到结束单调递增或者单调递减的部分下标拿到,如果递增的则不变递减的把单调递减部分List反转 使其变为递增(java.util.ComparableTimSort#countRunAndMakeAscending) 然后以前段的单调递增List为基础 之后的元素一个个与前段基础List二分查找插入位置进行插入(java.util.ComparableTimSort#binarySort),最终结果得出。
对于上述,单调递增数据部分为 2213,2214,2235,所以结束的下标为3,所以得到基础递增部分,一次按着红色部分进行排序

所以ra 最后得到排好的序列为  0       225    228    229    2211    2212    2213    2214    2215    2220    2229    2233    2235    2237    2241    2245

同理rb 最后得到排好的顺序为 225    226    227    2216    2217    2218    2221    2222    2223    2227    2232    2236    2240    2246    2247    2248

合并RUN

 合并2个相邻的run需要临时存储空闲,临时存储空间的大小是2个run中较小的run的大小。Timsort算法先将较小的run复制到这个临时存储空间,然后用原先存储这2个run的空间来存储合并后的run

首先以ra 最大值2245去rb里面寻找位置,rb里面最小值到ra 寻找位置

这时候会发现绿色部分,已经不再需要我们再排序了,以为ra 绿色部分比rb 最小值还小,rb 的绿色部分比ra的最大值还大

那么需要我们进行排序的部分就是中间无底色部分,显然需要排序的rb所占有的元素更少些,那么所以把rb的数据做一个备份 temp 用一个大小为16的数组  因为备份的是rb 所以rb要先覆盖

所以我们得到如下

比较temp的2240 和ra的2245 ,我们得到了ra的2245>temp 的2240,这时候计数ra 连续胜利一次,并且把ra的2245 写入到rb的未排好序位置,排序后如下

比较temp的2240 和ra的2241,我们得到ra的2241> temp 的2240.计数ra 连续胜利一次,并且把ra的2241 写入到rb的未排好序位置,排序后如下

比较temp 的2240 和ra的2237,我们得到temp的2240 大于ra的2237,计数temp 连续胜利一次,并且把temp的2240 写入到rb的未排好序位置,排序后如下

比较temp 的2236 和ra的2237,我们得到ra的2237 大于temp的2236,计数ra 连续胜利一次,并且把ra的2237 写入到rb的未排好序位置,排序后如下

中间省略若干步骤.........

来看关键步骤吧

此时红色下划线部分都是ra原来的元素,所以呢ra七连胜了,此时触发了Galloping模式,这也就是这个程序报错的关键点了,只有触发了这个模式并且数据结构满足有两个相等情况,才会抛出异常Comparison method violates its general contract!

我们来看下为啥会出现矛盾呢?

在Timsort中,Galloping指的是一种在合并两个已排序的子序列(称为run)时,用于跳过不必要比较的策略,当算法试图将一个元素插入到另一个已排序的子序列中时,如果它发现当前元素小于子序列的第一个元素,它会继续向后搜索,直到找到一个合适的插入位置或到达子序列的末尾。这个过程中跳过多个元素的比较就称为Galloping。

在这里因为ra已经连续赢了7次,那么temp 的剩余元素225,226,227就直接用最小的225来对比ra的225了, 因为最初程序里的排序算法是  list.sort((o1, o2) -> o1 > o2 ? 1 : -1)

所以整体的下移到ra 黄色部分 得到

此时temp 全部排完了,但是temp的225确被排序到ra的225的右侧了,按我们之前的排序规则(o1, o2) -> o1 > o2 ? 1 : -1,225 对比225应该是返回-1, 所以temp的225应该是排在ra225的左边,所以程序自相矛盾了,抛出异常Comparison method violates its general contract!

这篇关于Comparison method violates its general contract! 神奇的报错的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1082242

相关文章

IDEA编译报错“java: 常量字符串过长”的原因及解决方法

《IDEA编译报错“java:常量字符串过长”的原因及解决方法》今天在开发过程中,由于尝试将一个文件的Base64字符串设置为常量,结果导致IDEA编译的时候出现了如下报错java:常量字符串过长,... 目录一、问题描述二、问题原因2.1 理论角度2.2 源码角度三、解决方案解决方案①:StringBui

Python Jupyter Notebook导包报错问题及解决

《PythonJupyterNotebook导包报错问题及解决》在conda环境中安装包后,JupyterNotebook导入时出现ImportError,可能是由于包版本不对应或版本太高,解决方... 目录问题解决方法重新安装Jupyter NoteBook 更改Kernel总结问题在conda上安装了

Python安装时常见报错以及解决方案

《Python安装时常见报错以及解决方案》:本文主要介绍在安装Python、配置环境变量、使用pip以及运行Python脚本时常见的错误及其解决方案,文中介绍的非常详细,需要的朋友可以参考下... 目录一、安装 python 时常见报错及解决方案(一)安装包下载失败(二)权限不足二、配置环境变量时常见报错及

MySQL报错sql_mode=only_full_group_by的问题解决

《MySQL报错sql_mode=only_full_group_by的问题解决》本文主要介绍了MySQL报错sql_mode=only_full_group_by的问题解决,文中通过示例代码介绍的非... 目录报错信息DataGrip 报错还原Navicat 报错还原报错原因解决方案查看当前 sql mo

解决IDEA使用springBoot创建项目,lombok标注实体类后编译无报错,但是运行时报错问题

《解决IDEA使用springBoot创建项目,lombok标注实体类后编译无报错,但是运行时报错问题》文章详细描述了在使用lombok的@Data注解标注实体类时遇到编译无误但运行时报错的问题,分析... 目录问题分析问题解决方案步骤一步骤二步骤三总结问题使用lombok注解@Data标注实体类,编译时

linux报错INFO:task xxxxxx:634 blocked for more than 120 seconds.三种解决方式

《linux报错INFO:taskxxxxxx:634blockedformorethan120seconds.三种解决方式》文章描述了一个Linux最小系统运行时出现的“hung_ta... 目录1.问题描述2.解决办法2.1 缩小文件系统缓存大小2.2 修改系统IO调度策略2.3 取消120秒时间限制3

Kafka拦截器的神奇操作方法

《Kafka拦截器的神奇操作方法》Kafka拦截器是一种强大的机制,用于在消息发送和接收过程中插入自定义逻辑,它们可以用于消息定制、日志记录、监控、业务逻辑集成、性能统计和异常处理等,本文介绍Kafk... 目录前言拦截器的基本概念Kafka 拦截器的定义和基本原理:拦截器是 Kafka 消息传递的不可或缺

解决systemctl reload nginx重启Nginx服务报错:Job for nginx.service invalid问题

《解决systemctlreloadnginx重启Nginx服务报错:Jobfornginx.serviceinvalid问题》文章描述了通过`systemctlstatusnginx.se... 目录systemctl reload nginx重启Nginx服务报错:Job for nginx.javas

VMWare报错“指定的文件不是虚拟磁盘“或“The file specified is not a virtual disk”问题

《VMWare报错“指定的文件不是虚拟磁盘“或“Thefilespecifiedisnotavirtualdisk”问题》文章描述了如何修复VMware虚拟机中出现的“指定的文件不是虚拟... 目录VMWare报错“指定的文件不是虚拟磁盘“或“The file specified is not a virt

使用Vue.js报错:ReferenceError: “Vue is not defined“ 的原因与解决方案

《使用Vue.js报错:ReferenceError:“Vueisnotdefined“的原因与解决方案》在前端开发中,ReferenceError:Vueisnotdefined是一个常见... 目录一、错误描述二、错误成因分析三、解决方案1. 检查 vue.js 的引入方式2. 验证 npm 安装3.