号外,号外 -几乎所有的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说,他从中学到的教训是谦卑:哪怕一个简单的程序都很难写对,而整个社会却运行在庞大而复杂的代码上面。

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

号外,号外 -几乎所有的binary search和mergesort都有错的更多相关文章

  1. [数据结构]——二叉树(Binary Tree)、二叉搜索树(Binary Search Tree)及其衍生算法

    二叉树(Binary Tree)是最简单的树形数据结构,然而却十分精妙.其衍生出各种算法,以致于占据了数据结构的半壁江山.STL中大名顶顶的关联容器--集合(set).映射(map)便是使用二叉树实现 ...

  2. Leetcode 笔记 99 - Recover Binary Search Tree

    题目链接:Recover Binary Search Tree | LeetCode OJ Two elements of a binary search tree (BST) are swapped ...

  3. Leetcode 笔记 98 - Validate Binary Search Tree

    题目链接:Validate Binary Search Tree | LeetCode OJ Given a binary tree, determine if it is a valid binar ...

  4. Leetcode: Convert sorted list to binary search tree (No. 109)

    Sept. 22, 2015 学一道算法题, 经常回顾一下. 第二次重温, 决定增加一些图片, 帮助自己记忆. 在网上找他人的资料, 不如自己动手. 把从底向上树的算法搞通俗一些. 先做一个例子: 9 ...

  5. [LeetCode] Closest Binary Search Tree Value II 最近的二分搜索树的值之二

    Given a non-empty binary search tree and a target value, find k values in the BST that are closest t ...

  6. [LeetCode] Closest Binary Search Tree Value 最近的二分搜索树的值

    Given a non-empty binary search tree and a target value, find the value in the BST that is closest t ...

  7. [LeetCode] Verify Preorder Sequence in Binary Search Tree 验证二叉搜索树的先序序列

    Given an array of numbers, verify whether it is the correct preorder traversal sequence of a binary ...

  8. [LeetCode] Lowest Common Ancestor of a Binary Search Tree 二叉搜索树的最小共同父节点

    Given a binary search tree (BST), find the lowest common ancestor (LCA) of two given nodes in the BS ...

  9. [LeetCode] Binary Search Tree Iterator 二叉搜索树迭代器

    Implement an iterator over a binary search tree (BST). Your iterator will be initialized with the ro ...

随机推荐

  1. python基础----文件处理

    一.文件处理流程 打开文件,得到文件句柄并赋值给一个变量 通过句柄对文件进行操作 关闭文件 正趣果上果 Interesting fruit fruit 词:郭婞 曲:陈粒 编曲/混音/和声:燕池 萧: ...

  2. Yii2 数据库基本操作

    //1.简单查询  $admin=Admin::model()->findAll($condition,$params);  $admin=Admin::model()->findAll( ...

  3. Python多线程、进程、协程

    本节内容 操作系统发展史介绍 进程.与线程区别 python GIL全局解释器锁 线程 语法 join 线程锁之Lock\Rlock\信号量 将线程变为守护进程 Event事件 queue队列 生产者 ...

  4. Mysql外键设置中的CASCADE、NO ACTION、RESTRICT、SET NULL

    转: Mysql外键设置中的CASCADE.NO ACTION.RESTRICT.SET NULL 2017年06月11日 10:03:13 雅静8 阅读数:5491   版权声明:本文为博主原创文章 ...

  5. web项目中解决post乱码和get乱码的方法

    前提复习编码问题产生的原因: 1.  什么是URL编码. URL编码是一种浏览器用来打包表单输入的格式,浏览器从表单中获取所有的name和其对应的value,将他们以name/value编码方式作为U ...

  6. 堡垒机初识--paramiko模块

    一.paramiko模块 Python的paramiko模块基于SSH用于连接远程服务器并执行相关的操作. 1.1 在windows上安装paramiko模块 测试环境: win10 , python ...

  7. linux 文件IO

    1.文件描述符 (1)文件描述符的本质是一个数字,这个数字本质上是进程表中文件描述符表的一个表项,进程通过文件描述符作为index去索引查表得到文件表指针,再间接访问得到这个文件对应的文件表.(2)文 ...

  8. HTML+CSS基础小笔记再整理

    1. font的两个必须要写的:font-size 和 font-family text-indent 首行缩进(em)1em=一个文字大小 text-algin 对齐方式:left.center.r ...

  9. 在使用hibernate注解的时候,想对double类型的字段进行精度约束

    @Column(name = "price",precision = 10,scale = 2) public double getPrice() { return price; ...

  10. gulp教程之gulp-autoprefixer

    现在浏览器的种类很多,不同浏览器的内核不同,同一浏览器也有很多不同的版本,很多css属性为了兼容浏览器需要加上特定的前缀,比如chrome的前缀是-webkit-,firefox前缀是-moz-等. ...