二分查找 : 那个隐藏了 10 年的 Java Bug

一个偶然的机会,我想起以前还在谷歌上班的时候,有时候大家会在饭桌上讨论最新想出来的一些面试题。在众多有趣又有难度的题目中,有一道老题却是大家都纷纷选择避开的,那就是去实现二分查找。

因为它很好写,却很难写对。可以想象问了这道题后,在5分钟之内面试的同学会相当自信的将那一小段代码交给我们,剩下的就是考验面试官能否在更短的时间内看出这段代码的bug了。

二分查找是什么呢,这个不只程序员,其他很多非技术人员也会。比如我想一个1到100以内的数,你来猜,我告诉你每次猜的是大了还是小了,你会先猜50,然后25, 然后。。。用不了几个问题就猜出来了。1到100范围太小的话,我们放大点猜个人名,你问中国人外国人,古代人现代人,男的女的,用不了几个问题也问出来了。在计算机里,则是在一个有序数组里面,不断通过二分的方法缩小关键字的可能下标范围。当然了,我们不一定在一个有序数组里查找,也可以在一个很大的状态空间里,去查找一个单调函数的取值。这样的做法,似乎编个程序很容易实现,但是,

D.Knuth大神说了:Although the basic idea of binary search is comparatively straightforward, the details can be surprisingly tricky 虽然二分查找的基本思想相对来说很直接,但具体实现起来有特别多的坑。

另一位大神,编程珠玑的作者Jon Bentley,他做了我们在文章开头不敢做的事,他布置作业让他的学生们写二分查找,然后他一个个来看。结果呢,他发现90%是错的。因此在他的编程珠玑这本书中,专门有一章讲解了二分查找,虽然他的范例仍然是错的,见下面的Java Bug。埋下这个bug的人,也正式Jon Bentley的学生。

还有好事者,更是找了许多教科书,发现20本教科书里面,只有5本是写对了的,于是他发了一篇文章到ACM。当然这是早在1988年的时候。

然而这些都不算啥,更能让人感觉幸灾乐祸的是,Java库里面的二分查找,有一个埋藏了10年之久的bug。这个bug呢,在 java.util.Arrays.binarySearch 里面,虽然这个bug的修复也已经是10年前的事了。那么我们来看下当年的错误代码吧。

大家可能很难看出来,那毕竟这个bug藏了10年,不太容易发现。问题就在于

1

int mid = (low + high) / 2;

这里。low + high 是会溢出的。只要这个数组我们开的足够大,比如1100000000,就能重现这个问题,虽然这需要我们费点内存。因此正确的解法是:int mid = (low + high) >>> 1; 三个>,无符号位移的意思。正如修复bug的同学说的那样:

1

"Can‘t even compute average of two ints" is pretty embarrassing.

这个bug的链接在这里。

那么我们究竟如何来把二分查找写正确呢?我们二分查找中常见的错误除了上面的溢出之外,最多的是下面几类:

差1错误。我们的左端点应该是当前可能区间的最小范围,那么右端点是最大范围呢,还是最大范围+1呢。我们取了中间值之后,在缩小区间时,有没有保持左右端点的这个假设的一致性呢?
死循环。我们做的是整数运算,整除2了之后,对于奇数和偶数的行为还不一样,很有可能有些情况下我们并没有减小取值范围,而形成死循环。
退出条件。到底什么时候我们才觉得我们找不到呢?
我很喜欢二分查找这个案例。一个在理论上这么简单直接的算法,在计算机里实现却要考虑那么多实际的情况,除了理论的细化比如差1错误和退出条件,还有计算机的实际问题如整除2,死循环,以及上面提到的溢出。正如我们以前同事每天挂在嘴边的

You know the difference between in theory and in practice? In theory there’s no difference but in practice there are.

软件工程师,就是把现实生活用理论进行建模,然后再用现实来实现这样的理论。写出好的代码不容易,写出既让用户满意又好的代码,那更不容易。也许有时候,成就感就来自于此吧。

欢迎工作一到五年的Java工程师朋友们加入Java架构师:697558955

群内提供免费的Java架构学习资料(里面有高可用、高并发、高性能及分布式、Jvm性能调优、Spring源码,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多个知识点的架构资料)合理利用自己每一分每一秒的时间来学习提升自己,不要再用"没有时间“来掩饰自己思想上的懒惰!趁年轻,使劲拼,给未来的自己一个交代!

原文地址:https://blog.51cto.com/14233733/2369513

时间: 2024-10-20 00:13:41

二分查找 : 那个隐藏了 10 年的 Java Bug的相关文章

二分查找的两种实现方式(JAVA)

二分查找又称折半查找,优点是比较次数少,查找速度快,平均性能好:其缺点是要求待查表为有序表,且插入删除困难.因此,折半查找方法适用于不经常变动而查找频繁的有序列表.首先,假设表中元素是按升序排列,将表中间位置记录的关键字与查找关键字比较,如果两者相等,则查找成功:否则利用中间位置记录将表分成前.后两个子表,如果中间位置记录的关键字大于查找关键字,则进一步查找前一子表,否则进一步查找后一子表.重复以上过程,直到找到满足条件的记录,使查找成功,或直到子表不存在为止,此时查找不成功. 条件是:1.必须

二分查找的那些坑

听说很多人写不对二分查找,如果不好好总结一下,我大概也会是其中之一.. 历史上二分查找的bug 二分查找虽然原理很简单,实现起来却有很多的坑. <编程珠玑>的作者做实验发现90%的人写不对二分查找,然后亲手在该书里写下一个带 bug 的 binary search... 据说该 bug 在书里呆了二十年没人发现,而这本书还是一本人人交手称赞的好书. 然后 java 标准库里,一个和<编程珠玑>同样的 bug 在 2006 年才被发现.. 那这个 bug 是啥呢?是一个很好理解的问题

C++ 二分查找算法

#include<iostream> using namespace std; //二分查找法,查找一个数组的元素,并返回所在的位置的下标, //必须要是一个有序的数组, int select_arr(int arr[], int len, int arr_value) { while (1) { int left = 0; //数组的左侧下标 int right = len-1; //数组的右侧下标 while (left <= right) { int mid = (left + r

基于数组二分查找算法的实现

基于数组二分查找算法的实现 二分查找 查找 算法 赵振江 二分查找又称折半查找,优点是比较次数少,查找速度快,平均性能好:其缺点是要求待查表为有序表,且插入删除困难.因此,折半查找方法适用于不经常变动而查找频繁的有序列表.首先,假设表中元素是按升序排列,将表中间位置记录的关键字与查找关键字比较,如果两者相等,则查找成功:否则利用中间位置记录将表分成前.后两个子表,如果中间位置记录的关键字大于查找关键字,则进一步查找前一子表,否则进一步查找后一子表.重复以上过程,直到找到满足条件的记录,使查找成功

PAT甲级1010踩坑记录(二分查找)——10测试点未过待更新

题目分析: 首先这题有很多的坑点,我在写完之后依旧还有第10个测试点没有通过,而且代码写的不优美比较冗长勿喷,本篇博客用于记录写这道题的一些注意点 1.关于两个不同进制的数比大小一般采用将两个数都转化为10进制之后比较大小(下面统称已知进制数为N1,未知进制数为N2) 2.虽然两个数都只有10位,且每一位上的数字是从‘0’~‘z’,分别代表0~35,但是这并不意味值这题的进制范围就是2~36,radix完全有可能很大很大到long long,写‘0’~‘z’只是为了让结果计算出来相对小一些,并且

9.10 二分查找 gcc makefile gdb调试

 二分查找的迭代和递归实现: #include <stdio.h> #include <stdlib.h> int find1(int *a, int low , int high , int key)//迭代二分查找 { int mid = 0; while (low <= high) { mid = (low+high)/2; if (a[mid] == key) return mid; else if (a[mid] < key) low = mid + 1; e

【技术宅10】顺序二分查找算法

//顺序查找 //顺序查找是在一个已知无序队列中找出与给定关键字相同的数的具体位置.原理是让关键字与队列中的数从第一个开始逐个比较,直到找出与给定关键字相同的数为止. function search($array,$k){ $n = count($array);             //count函数用于计算数组中的元素个数 $array[$n] = $k;                //新建一个元素,并将k存放进去 for($i=0; $i<$n; $i++){ if($array[$

算法(第4版)-1.1.10 二分查找

总结:本小节通过二分查找的例子展示本书学习新算法的基本方法,研究新算法的原理.用例.必要性(模拟实际情况)和性能. 重点: 1.二分查找: import java.util.Arrays; public class BinarySearch { public static int rank(int key, int[] a) { int lo = 0; int hi = a.length - 1; while (lo <= hi) { // 被查找的键要么不存在,要么必然存在于a[lo..hi]

【C/C++学院】0723-32位与64位/调戏窗口程序/数据分离算法/内存检索/二分查找法/myVC

[送给在路上的程序员] 对于一个开发者而言,能够胜任系统中任意一个模块的开发是其核心价值的体现. 对于一个架构师而言,掌握各种语言的优势并阿赫利运用到系统中,由此简化系统的开发,是其架构生涯的第一步. 对于一个开发团队而言,能在短期内开发出用户满意的软件系统是起核心竞争力的体现. 每一个程序员都不能固步自封,要多接触新的行业,新的技术领域,突破自我. 32位与64位 地址与内存的关系 4G = 4*1024M = 4*1024*1024k = 4*1024*1024*1024 Byte字节 =