号外,号外 -几乎所有的binary search和mergesort都有错

简介: 号外,号外 -几乎所有的binary search和mergesort都有错这是Joshua Bloch(Effective Java的作者)在google blog上发的帖子。在说这个帖子之前,不得不强力重复Joshua Bloch的推荐:如果你还没有读过Programming Pearls (中文版叫《编程珠玑》)这本书,现在就去读吧。

号外,号外 -几乎所有的binary search和mergesort都有错

这是Joshua Bloch(Effective Java的作者)在google blog上发的帖子。在说这个帖子之前,不得不强力重复Joshua Bloch的推荐:如果你还没有读过Programming Pearls (中文版叫《编程珠玑》)这本书,现在就去读吧。如果你只读了一遍,现在就去再读一遍吧。

还是说回Joshua的文章。当初Programming Pearls的作者Jon Bentley到CMU做讲座。他叫在场的计算机系博士生们写出binary search的算法,然后当场分析了其中一份。当然,那份算法以及绝大部分人写的算法都错了。Jon Bentley在Programming Pearls里也提到,虽然1946年就有人发表binary search,但直到1962第一个正确运行的算法才写出来。这个小故事的关键教训就是写程序时要仔细考虑算法的不变量(invariant)。如果我记得没错,Programming Pearls第4章讲解了怎么证明binary search的正确性。当然,每本离散数学的教科书都会教我们列出pre-condition, invariant, 和post-condition,证明循环开始前pre-condition成立,循环中invariant始终成立,而循环结束后post-condition被满足,而几乎每本教科书(至少我看过的)都会用binary search作例子。所以有兴趣的自己去看吧,俺就不罗嗦了。

JDK里的binary search代码是这样实现的(Joshua Bloch本人写的)

     public static int binarySearch(int[] a, int key) {
         int low = 0;
         int high = a.length - 1;
 
         while (low <= high) {
             int mid = (low + high) / 2;
             int midVal = a[mid];

             if (midVal < key)
                 low = mid + 1;
             else if (midVal > key)
                 high = mid - 1;
             else
                 return mid; // key found
         }
         return -(low + 1);  // key not found.
     }

错误就在第6行:

             int mid = (low + high) / 2;

这行的问题是当low和high的和超过2^31-1, 也就是Java里最大整数值时,整数溢出就发生了,而mid就变成负数了, 于是JVM就抓狂了,于是ArrayIndexOutOfBoundsException就发生了。

当一个数组包含多过2^30元素时,这个错误就会被发现。那么大的数组在80年代Programming Pearls第一版写就的时候难以想象,但在现在却很常见。所以说,尽管1962年正确的binary search问世,现实却是直到现在流行系统里的binary search还有错。

解决的办法不难。把第6行改写成

            int mid = low + ((high - low) / 2);

或者

            int mid = (low + high) >>> 1;

C和C++里没有这个">>>",我们可以这样做:

            int mid = ((unsigned) (low + high)) >> 1。

那现在binary search就完全正确了么?我们还是不知道。我们得到的深刻教训是,仅仅证明一个程序正确是不够的。我们必须仔细测试。高德纳在写给Peter van Emde Boas的信里说,“上面那段程序可能有错。我只证明了它是正确的,但还没有测过”。人们往往用这段话来彰显高德纳的一丝不苟和学究气,谁知道这句话背后是高德纳深刻的洞察力。人们常说“理论上讲实践和理论没有差别。实践上讲,两者确有差别”,可为旁证。

binary search的这个错误同样会出现在其它“分而治之”的算法里,比如说mergesort。如果你有类似的算法代码,赶快修改吧。Joshua说,他从中学到的教训是谦卑:哪怕一个简单的程序都很难写对,而整个社会却运行在庞大而复杂的代码上面。

最后的总结很有意思:我们程序员需要各种帮助,别无它法。仔细设计很好。测试很好。形式化方法很好(不过我还是觉得有教授研究用形式化电子商务需求(比如用范畴论),纯粹无事找事)。代码评审很好,静态分析很好。但他们并不能帮我们彻底消除代码错误--他们将永远存在。我们半个世纪以来竭尽全力都不能消除一个程序错误。我们必须小心翼翼,防御性地编程,并且保持警醒。

目录
相关文章
|
7月前
|
自然语言处理 算法 Java
Java如何判断两句话的相似度类型MySQL的match
【9月更文挑战第1天】Java如何判断两句话的相似度类型MySQL的match
47 2
|
8月前
【Azure Developer】使用PowerShell Where-Object方法过滤多维ArrayList时候,遇见的诡异问题 -- 当查找结果只有一个对象时,返回结果修改了对象结构,把多维变为一维
【Azure Developer】使用PowerShell Where-Object方法过滤多维ArrayList时候,遇见的诡异问题 -- 当查找结果只有一个对象时,返回结果修改了对象结构,把多维变为一维
|
9月前
|
Java 数据库连接 应用服务中间件
表单数据返回不到,HTTP状态 404 - 未找未找到,解决方法,针对这个问题,写一篇文章,理一下思路,仔细与原项目比对,犯错的原因是Mapper层的select查询表单数据写错,注意打开的路径对不对
表单数据返回不到,HTTP状态 404 - 未找未找到,解决方法,针对这个问题,写一篇文章,理一下思路,仔细与原项目比对,犯错的原因是Mapper层的select查询表单数据写错,注意打开的路径对不对
|
算法 数据挖掘 Python
如何在 Python 中查找两个字符串之间的差异位置?
如何在 Python 中查找两个字符串之间的差异位置?
354 0
|
存储 数据库
laravel-admin 查询过滤时间戳(数据库使用int类型)不起作用案例复现及解决办法
laravel-admin 查询过滤时间戳(数据库使用int类型)不起作用案例复现及解决办法
318 0
laravel-admin 查询过滤时间戳(数据库使用int类型)不起作用案例复现及解决办法
|
SQL 关系型数据库 MySQL
mysql sum函数中对两字段做运算时有null时的情况
mysql sum函数中对两字段做运算时有null时的情况
222 0
|
SQL 关系型数据库 MySQL
MySql 使用 NOT IN 返回值包含null值,返回数据不全
MySql 使用 NOT IN 返回值包含null值,返回数据不全
305 0
MySql 使用 NOT IN 返回值包含null值,返回数据不全
|
SQL Python
Python基础记录下字符串模糊匹配的方式
使用Python的difflib库中get_close_matches方法
316 0
Python基础记录下字符串模糊匹配的方式
|
存储 关系型数据库 MySQL
一文速学-玩转MySQL时间运算函数以及时间匹配操作详解+实例代码
一文速学-玩转MySQL时间运算函数以及时间匹配操作详解+实例代码
361 0
一文速学-玩转MySQL时间运算函数以及时间匹配操作详解+实例代码
|
Oracle 关系型数据库
oracle按code编码长度查询代码展现层级关系(给字段前加空格)
学习oracle按code编码长度查询代码展现层级关系(给字段前加空格)
172 0
oracle按code编码长度查询代码展现层级关系(给字段前加空格)
AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等