`
dandy
  • 浏览: 67310 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

一种疑似排序的惊人传奇

    博客分类:
  • java
阅读更多
出自《java puzzle》


下面的程序使用定制的比较器,对一个由随机挑选的Integer实例组成的数组进行排序,然后打印了一个描述了数组顺序的单词。回忆一下,Comparator接口只有一个方法,即compare,它在第一个参数小于第二个参数时返回一个负数,在两个参数相等时返回0,在第一个参数大于第二个参数时返回一个整数。这个程序是展示5.0版特性的一个样例程序。它使用了自动包装和解包、泛型和枚举类型。那么,它会打印出什么呢?

import java.util.*;
public class SuspiciousSort {
public static void main(String[ ] args) {
Random rnd = new Random();
Integer[ ] arr = new Integer[100];
for (int i = 0; i < arr.length; i++)
arr[i] = rnd.nextInt();
Comparator<Integer> cmp = new Comparator<Integer>() {
public int compare(Integer i1, Integer i2) {
return i2 - i1;
}
};
Arrays.sort(arr, cmp);
System.out.println(order(arr));
}
enum Order { ASCENDING, DESCENDING, CONSTANT, UNORDERED };
static Order order(Integer[ ] a) {
boolean ascending = false;
boolean descending = false;
for (int i = 1; i < a.length; i++) {
ascending |= a[i] > a[i-1];
descending |= a[i] < a[i-1];
}
if (ascending && !descending)
return Order.ASCENDING;
if (descending && !ascending)
return Order.DESCENDING;
if (!ascending)
return Order.CONSTANT; // All elements equal
return Order.UNORDERED; // Array is not sorted
}
}


该程序的main方法创建了一个Integer实例的数组,并用随机数对其进行了初始化,然后用比较器cmp对该数组进行排序。这个比较器的compare方法将返回它的第二个参数减去第一个参数的值,如果第二个参数表示的是比第一个参数大的数值,其返回值就是正的;如果这两个参数相等,其返回值为0;如果第二个参数表示的是比第一个参数小的数值,其返回值就是负的。这种行为正好与compare方法通常的做法相反,因此,该比较器应该施加的是降序排列。
在对数组排序之后,main方法将该数组传递给了静态方法order,然后打印由这个方法返回的结果。该方法在数组中所有的元素都表示相同的数值时,返回CONSTANT;在数组中每一对毗邻的元素中第二个元素都大于等于第一个元素时,返回ASCENDING;在数组中每一对毗邻的元素中第二个元素都小于等于第一个元素时,返回DESCENDING;在这些条件都不满足时,返回UNORDERED。尽管理论上说,数组中的100个随机数有可能彼此都相等,但是这种奇特现象发生的非常小:232×99分之一,即大约5×10953分之一。因此,该程序看起来应该打印DESCENDING。如果你运行该程序,几乎可以肯定你将看到它打印的是UNORDERED。为什么它会产生如此的行为呢?
order方法很直观,它并不会说谎。Arrays.sort方法已经存在许多年了,它工作得非常好。现在只有一个地方能够发现bug了:比较器。乍一看,这个比较器似乎不可能出错。毕竟,它使用的是标准的惯用法:如果你有两个数字,你想得到一个数值,其符号表示它们的顺序,那么你可以计算它们的差。这个惯用法至少从1970年代早期就一直存在了,它在早期的UNIX里面被广泛地应用。遗憾的是,这种惯用法从来都没有正确地工作过。本谜题也许应该称为“白痴一般的惯用法的案例”。这种惯用法的问题在于定长的整数没有大到可以保存任意两个同等长度的整数之差的程度。当你在做两个int或long数值的减法时,其结果可能会溢出,在这种情况下我们就会得到错误的符号。
例如,请考虑下面的程序:

public class Overflow {
public static void main(String[] args){
int x = -2000000000;
int z = 2000000000;
System.out.println(x - z);
}
}


很明显,x比z小,但是程序打印的是294967296,它是一个正数。既然这种比较的惯用法是有问题的,那么为什么它会被如此广泛地应用呢?因为它在大多数时间里可以正常工作的。它只在用来来进行比较的两个数字的差大于Integer.MAX_VALUE的时候才会出问题。这意味着对于许多应用而言,在实际使用中是不会看到这种错误的。更糟的是,它们被观察到的次数少之又少,以至于这个bug永远都不会被发现和订正。
那么这对于我们的程序的行为意味着什么呢?如果你查阅一下Comparator的文档,你就会看到它所实现的排序关系必须是可传递的(transitive),换句话说,(compare(x,y) > 0)&&(compare(y,z) > 0)蕴含着compare(x,z) > 0。如果我们取Overflow例子中的x和z,并取y为0,那么我们的比较器在这些数值上就违反了可传递性。事实上,在所有随机选取的int数值对中,有四分之一该比较器都会返回错误的值。用这样的比较器来执行一个搜索或排序,或者用它去排序一个有序的集合,都会产生不确定的行为,就像我们在运行本谜题的程序时所看到的那样。出于数学上的倾向性,Comparator.compare方法的一般约定要求比较器要产生一个全序(total order),但是这个比较器在数个计算上都未能做到这一点。
我们可以通过替换遵守上述一般约定的Comparator实现来订正我们的程序。因为我们只是想要反转自然排序的顺序,所以我们甚至可以不必编写我们自己的比较器。Collection类提供了一个可以产生这种顺序的比较器。如果你用Arrays.sort(arr,Collections.reverseOrder())来替代最初的Arrays.sort调用,该程序就可以打印出我们所期望的DESCENDING。
或者,你可以编写你自己的比较器。下面的代码并不“聪明”,但是它可以工作,从而使该程序可以打印出我们所期望的DESCENDING:
public int compare(Integer i1, Integer i2) {
return (i2 < i1 ? -1 : (i2 == i1 ? 0 :1));
}
本谜题有数个教训,最具体的是:不要使用基于减法的比较器,除非你能够确保要比较的数值之间的差永远不会大于Integer.MAX_VALUE [EJ Item 11]。更一般地讲,要意识到int的溢出。另一个教训是你应该避免“聪明”的代码。应该努力去编写清晰正确的代码,不要对它作任何优化,除非该优化被证明是必需的[EJ Item 37]。
2
1
分享到:
评论

相关推荐

    一种用于防爆储运罐的疑似爆炸物支撑托盘的制作方法.docx

    一种用于防爆储运罐的疑似爆炸物支撑托盘的制作方法.docx

    5G远程机器人超声诊断疑似新型冠状病毒感染肺炎一例.pdf

    本文介绍了一例利用5G远程机器人超声技术诊断疑似新型冠状病毒感染性肺炎的病例,揭示了远程医疗、人工智能和医学影像诊断领域的新进展。报告详细描述了一位老年患者的病情、诊疗过程和最终结果,体现了5G技术在医疗...

    面向肺癌CAD的CT图像疑似病灶检测算法.pdf

    对于肺癌而言,CT扫描是一种有效的筛查和诊断手段,但随之而来的大量图像数据给诊断工作带来了巨大挑战。因此,提高图像中疑似病灶检测的准确性,减少医生的漏诊和误诊几率,是当前医学图像处理研究的热点之一。 ...

    人工智能肺炎辅助诊断系统在新型冠状病毒肺炎疑似病例CT筛查中的应用价值.pdf

    研究通过回顾性分析2020年1月至3月期间郑州大学第一附属医院收治的206例疑似病例的胸部CT影像和临床数据。确诊患者包括轻型、普通型、重型和危重型患者,而排除的则包括其他类型肺炎和胸部CT未见明显异常者。 研究...

    疑似传染病人转诊记录表.doc

    在医疗信息化领域,疑似传染病人转诊记录表是医疗机构中非常关键的一环,主要用于管理和跟踪可能患有传染病的患者。此文档的主要目的是确保在病患转诊过程中,信息的准确无误,以便接收医院能及时采取适当的隔离和...

    疑似传染病病人情况登记表75703.doc

    疑似传染病病人情况登记表75703.doc

    一种基于深度学习的层次化钓鱼网站检测方法.pdf

    一种基于深度学习的层次化钓鱼网站检测方法 深度学习在近些年来获得了快速发展,已经广泛应用于图像识别、自然语言处理、语音识别等领域。然而,在网络安全领域,深度学习的应用仍在不断扩展,例如钓鱼网站检测就是...

    抑郁症网络社交与疑似抑郁微博初步筛选算法.pdf

    总结来说,这篇论文探讨了如何运用社交网络数据,尤其是大学生微博,开发出一种初步筛选疑似抑郁内容的算法。通过抑郁关键词的扩展和语义分析,该算法能有效地从大规模数据中找出可能反映抑郁情绪的微博,从而为抑郁...

    一种基于混合高斯模型的运动目标阴影检测策略

    基于混合高斯模型,提出一种双重阴影消除策略,首先通过HSV模型下的颜色夹角确定疑似阴影,再对运动目标和疑似阴影进行混合高斯建模而消除实际阴影。通过实验表明,该策略在不影响目标识别的情况下可以较好的检测并...

    云计算环境下入侵疑似边界问题改进算法.pdf

    为了提高网络安全性能,有研究者提出了一种基于模糊网络阈值计算的入侵疑似边界确定算法。这种方法通过计算模糊网络阈值,来确定云计算环境下入侵检测中的疑似边界具体参数。 模糊网络阈值计算法是基于模糊逻辑的...

    一种基于帧差法的夜间车辆检测方法

    综上所述,本文提出了一种基于帧差法的夜间车辆检测方法,通过有效抑制光晕影响、引入“疑似车辆”的概念以及根据运动目标分布情况判断车型等手段,大大提高了夜间车辆检测的准确性。该方法不仅能够适应复杂的夜间...

    一种适于FPGA实现的中值滤波及软硬件协同仿真.pdf

    本文介绍了一种适合在FPGA(现场可编程门阵列)上实现的中值滤波改进算法,并通过软硬件协同仿真验证了算法的处理效果。中值滤波是一种图像处理技术,用于去除图像噪声,特别是在去除脉冲干扰和图像扫描噪声方面效果...

    一种有效的用于疲劳驾驶检测的人眼定位算法

    疲劳驾驶是全球交通安全的重大隐患之一,据统计,美国每年因疲劳驾驶而发生的交通事故数量惊人。鉴于眼睛的状态能够反映驾驶员的疲劳程度,因此,准确快速地定位人眼对于判断驾驶员的疲劳状态至关重要。 #### 方法...

    一种对二值图像切割分块的目标识别方1

    本文提出了一种在目标筛选法基础上利用目标的外接正矩形获取相应的参数,并依据这些参数进行目标几何特性分析、剔除伪目标、保留真目标及疑似目标,直到最终识别目标的方法。 目标识别是图像处理的高级阶段,快速、...

    一种改进的边界法向量叠加疑似肺结节提取 (2010年)

    对边界法向量叠加法进行改进,提出一种基于局部形状约束和自适应设定法向量大小的边界法向量叠加疑似肺结节提取方法。首先用自适应阈值方法对肺实质图像进行分割,得到初始 ROI区域 ;然后判断初始 ROI区域边界点的局部...

    骨肽注射液疑似过敏反应影响因素分析:基于7家医疗机构的真实数据.pdf

    骨肽注射液是一种广泛用于骨疾病治疗的药物,但国家药品不良反应监测中心的数据表明,该药物可能导致严重的过敏反应。这不仅增加了患者的住院时间,加重了他们的经济负担,甚至可能威胁到患者的生命安全。针对这一...

Global site tag (gtag.js) - Google Analytics