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

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

目录
相关文章
|
8月前
|
JavaScript 算法 开发者
如何用JS实现在网页上通过鼠标移动批量选择元素的效果?
本文介绍了类似电脑桌面通过鼠标选择多个图标的实现原理。主要通过监听mousedown、mousemove和mouseup事件,动态调整选择框大小并计算与元素的重叠情况。提供了角重叠和相交重叠的检测方法,并附有示例代码和在线演示链接,方便开发者参考与测试。
270 56
|
6月前
|
人工智能 自然语言处理 运维
AI agent跨平台云资源智能管理终端是什么
随着多云架构和混合IT环境的普及,企业面临跨平台资源协同效率低、操作复杂等问题。为此,跨平台云资源智能管理终端应运而生。它通过模块化架构与自动化引擎,将异构云环境中的资源统一管理,并提供对话式交互、批量操作与智能策略编排能力。典型产品如Chaterm,支持自然语言指令输入,实现从任务规划到执行反馈的闭环体验。其应用场景涵盖大规模服务器集群管理、跨云资源调度、复杂环境自动化配置等,显著提升效率与可靠性。实施时需关注兼容性、扩展性及安全性,建议从试点入手逐步推广,优化企业运维流程。
317 5
|
人工智能
AI大模型初体验
为了实现真正的A,需不断学习以提升能力。
344 3
AI大模型初体验
|
机器学习/深度学习 物联网 异构计算
ExVideo+CogVideoX,更长、更优!再次升级的开源视频生成能力
DiffSynth-Studio 再次为 CogVideoX 带来新的增强模块——ExVideo-CogVideoX-LoRA-129f-v1
|
XML Java Android开发
34. 【Android教程】菜单:Menu
34. 【Android教程】菜单:Menu
582 2
|
11月前
|
人工智能 文字识别 BI
多模态数据信息提取解决方案评测报告
《多模态数据信息提取解决方案评测报告》概述了该方案在商业智能、内容审核等领域的应用。报告指出,该方案通过AI技术解析多种格式文件,提升数据处理效率。部署界面直观易用,但数据类型选择和复杂配置需优化。部署文档详尽,涵盖环境准备到验证,但在操作系统差异方面可加强指导。函数应用模板简化部署,适合非技术人员,但对于高级用户细节说明不足。官方示例展示了系统的强大功能,但在长篇文本和低质量图片处理上有改进空间。整体上,该方案表现良好,具有灵活性和可移植性,但仍需进一步优化以满足特定领域需求。
190 8
|
传感器 JavaScript 算法
Higress 全新 Wasm 运行时,性能大幅提升(二)
WAMR 是最早由 Intel 团队开发,在字节码联盟(Bytecode Alliance,面向 Wasm 软件生态的非盈利组织)下的一个广受欢迎的 WebAssembly 运行时开源项目。
329 2
|
Java
软件工程设计原理里氏替换原则 ,具体实现及JAVA代码举例
里氏替换原则(Liskov Substitution Principle, LSP)是面向对象设计的基本原则之一,由Barbara Liskov提出。这个原则指出,如果类 S 是类 T 的子类型,则程序中使用 T 的对象的地方都可以不经修改地使用 S 的对象。换句话说,子类的对象应该能够替换掉它们的父类对象,而不影响程序的正确性。这个原则强调了继承关系中的行为兼容性,保证了基类和派生类之间的正确抽象和继承关系。
273 3
|
XML JSON 安全
借助API接口实现自营商城上货采集,无货源模式采集商品
在无货源模式的自营商城中,通过API接口实现商品采集是一个高效且灵活的方式。这种方式允许商家直接从供应商或其他电商平台的API接口中获取商品信息,然后将这些信息导入到自己的商城中,无需自己拥有实际的库存。
|
Java Maven
thymeleaf的maven依赖
thymeleaf的maven依赖

热门文章

最新文章