号外,号外 -几乎全部的binary search和mergesort都有错java
这是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就发生了。post
当一个数组包含多过2^30元素时,这个错误就会被发现。那么大的数组在80年代Programming Pearls初版写就的时候不可思议,但在如今却很常见。因此说,尽管1962年正确的binary search问世,现实倒是直到如今流行系统里的binary search还有错。测试
解决的办法不难。把第6行改写成google
int mid = low + ((high - low) / 2);
或者.net
int mid = (low + high) >>> 1;
C和C++里没有这个">>>",咱们能够这样作:设计
int mid = ((unsigned) (low + high)) >> 1。
那如今binary search就彻底正确了么?咱们仍是不知道。咱们获得的深入教训是,仅仅证实一个程序正确是不够的。咱们必须仔细测试。高德纳在写给Peter van Emde Boas的信里说,“上面那段程序可能有错。我只证实了它是正确的,但尚未测过”。人们每每用这段话来彰显高德纳的一丝不苟和学究气,谁知道这句话背后是高德纳深入的洞察力。人们常说“理论上讲实践和理论没有差异。实践上讲,二者确有差异”,可为旁证。
binary search的这个错误一样会出如今其它“分而治之”的算法里,好比说mergesort。若是你有相似的算法代码,赶快修改吧。Joshua说,他从中学到的教训是谦卑:哪怕一个简单的程序都很难写对,而整个社会却运行在庞大而复杂的代码上面。
最后的总结颇有意思:咱们程序员须要各类帮助,别无它法。仔细设计很好。测试很好。形式化方法很好(不过我仍是以为有教授研究用形式化电子商务需求(好比用范畴论),纯粹无事找事)。代码评审很好,静态分析很好。但他们并不能帮咱们完全消除代码错误--他们将永远存在。咱们半个世纪以来不遗余力都不能消除一个程序错误。咱们必须当心翼翼,防护性地编程,而且保持警醒。