当前位置:  开发笔记 > 编程语言 > 正文

实现二进制搜索有哪些陷阱?

如何解决《实现二进制搜索有哪些陷阱?》经验,为你挑选了4个好方法。

二进制搜索比它看起来更难实现."尽管二元搜索的基本思想相对简单,但细节却令人惊讶地难以理解......" - 唐纳德克努特.

哪些错误最有可能被引入新的二进制搜索实现?



1> ShreevatsaR..:

最近刚刚再次提出这个问题.除了Knuth的引用之外,"尽管二元搜索的基本思想相对简单,但细节可能会非常棘手",有一个令人震惊的历史事实(参见TAOCP,第3卷,第6.2.1节),二元搜索首次发布于1946年,第一次发布没有错误的二进制搜索是在1962年.并且有宾利的经验,当他在贝尔实验室和IBM这样的地方为专业程序员分配二进制搜索并给他们两个小时时,每个人都报告他们说得对,并且在检查他们的代码时,90%的人都有年复一年的错误.

也许除了斯特金定律之外,这么多程序员在二元搜索中犯错误的根本原因在于他们不够谨慎:编程珍珠称这是"编写你的代码,把它扔到墙上,并有质量保证或测试交易与错误"接近.并且存在很多错误的余地.不仅仅是这里提到的其他几个答案的溢出错误,而是逻辑错误.

以下是二进制搜索错误的一些示例.这绝不是详尽无遗的.(正如托尔斯泰在安娜卡列尼娜写的那样- "所有幸福的家庭都是一样的;每个不幸的家庭都以自己的方式不开心" - 每一个不正确的二元搜索程序都以自己的方式不正确.)

Pattis的

以下Pascal代码取自Richard E Pattis的二元搜索(1988)中的Textbook errors.他查看了二十本教科书并提出了这种二分法搜索(BTW,Pascal使用从1开始的数组索引):

PROCEDURE BinarySearch (A         : anArray,
                        Size      : anArraySize,
                        Key       : INTEGER,
                        VAR Found : BOOLEAN;
                        VAR Index : anArrayIndex);
Var Low, High : anArrayIndex;
BEGIN         
   LOW := 1;
   High := Size;

   REPEAT
      Index := (Low + High) DIV 2;
      If Key < A[Index]
         THEN High := Index - 1
         ELSE Low  := Index + 1
   UNTIL (Low > High) OR (Key = A[Index]);

   FOUND := (Low <= High)
END;

看起来不错?这有多个错误.在进一步阅读之前,看看你是否能找到它们.即使您第一次看到Pascal,您也应该能够猜出代码的作用.


他描述了许多程序所具有的五个错误,特别是上面的错误:

错误1:它不在O(log n)时间内运行,其中n = Size.在他们对Proper Programming Practice的热情中,一些程序员将二进制搜索作为函数/过程编写,并将其传递给数组.(这不是特定于Pascal;想象一下在C++中通过值而不是通过引用传递向量.)这是Θ(n)时间,只是为了将数组传递给过程,这会破坏整个目的.更糟糕的是,一些作者显然给出了递归二进制搜索,每次都传递一个数组,给出的运行时间为Θ(n log n).(这不是牵强附会;我实际上看过这样的代码.)

错误2:当size = 0时失败.这可能没问题.但是根据预期的应用程序,被搜索的列表/表可能缩小为0,并且必须在某处处理.

错误3:它给出了错误的答案.每当循环的最后一次迭代以Low = High开始时(例如,当Size = 1时),它设置Found:= False,即使Key它在数组中.

错误4:只要Key小于数组的最小元素,就会导致错误.(在Index变为1之后,它设置High为0等;导致越界错误.)

错误5:只要Key大于数组的最大元素,就会导致错误.(在Index变为之后Size,它设置Low为Size + 1等;导致越界错误.)

他还指出,"修复"这些错误的一些明显方法也是错误的.现实生活中的代码也常常具有这种属性,当程序员写错了东西,发现错误,然后"修复"它直到看起来正确而不仔细考虑.

在他试过的20本教科书中,只有5本有正确的二元搜索.在其余的15个中(他讽刺地说是16个),他发现了11个错误1的实例,6个错误2的实例,错误3和4中的两个,以及错误5中的一个.这些数字加起来超过15,因为其中有几个有多个错误.


更多例子

二进制搜索不仅用于搜索数组以查看它是否包含值,因此这里还有一个例子.我可能会更多地更新此列表.

假设你有一个增加(非递减)函数f:R-> R,并且(因为你想要一个f的根,比方说),你想找到最大的t那个f(t) < 0.看看你可以在下面找到多少错误:

float high = INF, low = 0;
while(high != low) {
   float mid = (low + high)/2;
   if(f(mid)>0) high=mid;
   else low=mid;
}
printf("%f", high);

(某些:[0,INF]中可能没有这样的t,如果f某个时间间隔为0,那么这是错误的,从不比较浮点数是否相等等)

如何编写二进制搜索

我曾经做过几次这样的错误 - 我编写二进制搜索的前几十次(在编程竞赛中遇到时间压力),大约有30%的时间存在错误 - 直到我找到了编写它的简单方法正确.(因为我记得),我没有错误的二进制搜索.诀窍很简单:

保持不变.

查找/决定并明确表示您的"低"和"高"变量在整个循环中满足的一些不变属性:之前,期间和之后.确保它永远不会被侵犯.当然,您还需要考虑终止条件.这在Pearls编程的第4章中有详细解释,它从半形式方法中导出二进制搜索程序.

例如,要稍微抽象出被检查的条件,假设您要查找x某些条件poss(x)为真的最大整数值.即使是问题定义的这种明确性也不仅仅是许多程序员的开端.(例如,poss(x)可能是a[x] ? v某些值v;这是为了找出排序数组a中的元素数量多于多少v.)然后,编写二进制搜索的一种方法是:

int lo=0, hi=n;
//INVARIANT: poss(lo) is true, poss(hi) is false
//Check and ensure invariant before starting binary search
assert(poss(lo)==true);
assert(poss(hi)==false);
while(hi-lo>1) {
    int mid = lo + (hi-lo)/2;
    if(poss(mid)) lo = mid;
    else hi = mid;
}
printf("%d \n",lo);

您可以添加更多断言语句和其他检查,但其基本思想是,因为你更新lomid 只有当你知道poss(mid)是真的,你保持不变的是poss(lo)始终为true.同样,您设置himid仅在poss(mid)false时,因此您保持poss(hi)始终为false 的不变量.分别考虑终止条件.(注意,当hi-lo为1时,mid是相同的lo.所以不要把循环写成while(hi>lo),否则你将有一个无限循环.)在循环结束时,你保证hi-lo最多为1,因为你永远保持你的不变性(poss(lo)是真的,poss(hi)是假的),它不能是0.而且,再次因为你的不变量,你知道这lo是返回/打印/使用的值.当然,还有其他方法可以编写二进制搜索,但保持不变是一种总是有帮助的技巧/规则.



2> Zach Scriven..:

以下是我能想到的一些内容:

在确定下一个间隔的边界时,逐个错误

处理重复项目,如果您想要返回数组中的第一个相等项目,而是返回后续相等项目

计算索引时的数字下溢/溢出,具有大型数组

递归非递归实现,您应该考虑的设计选择

这些是你的想法吗?



3> Dan Dyer..:

读这个.Java的二进制搜索实现在任何人发现它之前隐藏了差不多十年的错误.

这个bug是整数溢出.它没有引起人们的问题,因为几乎没有人在搜索足够大的数据结构.


那个bug太棒了.

4> Tiago..:

如果你附近有编程珍珠书,你应该查看第4章.

edit2:正如评论中所指出的,你可以下载我提到作者网站的章节草稿:http://www.cs.bell-labs.com/cm/cs/pearls/sketch04.html


除非在那本书中,否则实施中存在错误.具有讽刺意味的.
推荐阅读
Chloemw
这个屌丝很懒,什么也没留下!
DevBox开发工具箱 | 专业的在线开发工具网站    京公网安备 11010802040832号  |  京ICP备19059560号-6
Copyright © 1998 - 2020 DevBox.CN. All Rights Reserved devBox.cn 开发工具箱 版权所有