实施二分查找有哪些陷阱? [关闭]

2024-05-18

二分查找比看起来更难实现。 “虽然二分搜索的基本思想相对简单,但细节可能出人意料地棘手……”——Donald Knuth。

新的二分搜索实现中最有可能引入哪些错误?


这个问题是最近刚刚又问了一遍 https://stackoverflow.com/questions/6372037/binary-search-problems。除了 Knuth 引用的“虽然二分搜索的基本思想相对简单,但细节却出奇的棘手”之外,还有一个令人震惊的历史事实(参见 TAOCP,第 3 卷,第 6.2.1 节):二分搜索首次发表于1946年却首次发表二分查找没有错误1962 年。Bentley 的经验是,当他在贝尔实验室和 IBM 等地的专业程序员课程中布置二分搜索并给他们两个小时时,每个人都报告说他们做对了,在检查他们的代码时,90% 的人都说他们做对了年复一年地出现错误。

也许除了斯特金定律之外,这么多程序员在二分查找中犯错误的根本原因是他们不够小心:编程珍珠将此引用为“编写代码,将其扔到墙上,然后通过质量保证或测试来处理错误”的方法。而且有很大的出错空间。不仅仅是这里其他几个答案提到的溢出错误,还有逻辑错误。

以下是二分搜索错误的一些示例。这绝不是详尽无遗的。 (正如托尔斯泰在《安娜·卡列尼娜—“幸福的家庭都是相似的;不幸的家庭各有各的不幸”——每一个不正确的二分查找程序都有自己不正确的方式。)

Pattis

以下Pascal代码摘自论文二分查找中的教科书错误(1988) 理查德·E·帕蒂斯。他查看了 20 本教科书,并提出了这个二分搜索(顺便说一句,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,您也应该能够猜出代码的作用。


他描述了许多程序存在的五个错误,特别是上述错误:

Error 1:它不会在 O(log n) 时间内运行,其中 n = Size。出于对正确编程实践的热情,一些程序员将二分搜索编写为函数/过程,并将其传递给一个数组。 (这不是 Pascal 所特有的;想象一下在 C++ 中按值而不是按引用传递向量。)仅将数组传递给过程就需要 θ(n) 时间,这违背了整个目的。更糟糕的是,一些作者显然给出了递归的二分查找,每次传递一个数组,运行时间为 θ(n log n)。 (这并不牵强;我实际上见过这样的代码。)

Error 2:当 size = 0 时失败。这可能没问题。但根据预期的应用程序,正在搜索的列表/表格may缩小到0,必须在某个地方进行处理。

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

Error 4: 每当出现错误时Key小于数组的最小元素。 (后Index变为1,则设置High至 0 等;导致越界错误。)

Error 5: 每当出现错误时Key大于数组的最大元素。 (后Index变成Size,它设置Low大小+1等;导致越界错误。)

他还指出,一些“修复”这些错误的明显方法也被证明是错误的。现实生活中的代码也经常具有这种属性,当程序员写了一些不正确的东西,发现错误,然后“修复”它,直到它seemed没有仔细思考就正确。

在他尝试的 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% 的时间在某个地方出现错误 - 直到我找到了编写它的简单方法正确。从那以后(我记得)我就没有犯过二分搜索错误。技巧很简单:

保持不变式。

查找/决定并明确您的“低”和“高”变量在整个循环中满足的一些不变属性:之前、期间和之后。确保它永远不会被违反。当然你还需要考虑终止条件。这在第 4 章中有详细解释编程珍珠 which derives半正式方法的二分搜索程序。

例如,为了稍微抽象出正在检查的条件,假设您想找到最大的整数值x对于某些条件poss(x)是真的。甚至这种问题定义的明确性也超出了许多程序员的起点。 (例如,poss(x) may be 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);

您可以添加更多断言语句和其他检查,但基本思想是因为您更新lo to mid only当你知道的时候poss(mid)是真的,你保持不变poss(lo)总是正确的。同样,你设置hi to mid只有当poss(mid)是假的,所以你保持不变式poss(hi)总是假的。单独考虑终止条件。 (请注意,当hi-lo is 1, mid是相同的lo。所以不要将循环写为while(hi>lo),否则就会出现无限循环。)在循环结束时,可以保证hi-lo至多为 1,并且因为你始终保持不变(poss(lo)是真的并且poss(hi)是假的),它不能是 0。另外,再次由于你的不变量,你知道lo是要返回/打印/使用的值。当然,还有其他方法可以编写二分搜索,但维护不变量是一个总是有帮助的技巧/规则。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

实施二分查找有哪些陷阱? [关闭] 的相关文章

  • 2D形状识别与解析算法

    我正在寻找一种算法 用于从给定的一组 x y 点检测简单形状 如矩形 三角形 正方形和圆形 我还在寻找一种方法 一旦检测到 将路径转换为更干净的形状 我已经查遍了互联网 但没有找到任何 简单 的方法 几乎所有这些对于我的简单实现来说都是高级
  • 如何检测图像是否像素化

    之前有人在 SO 上提出过这样的问题 在Python中检测像素化图像 https stackoverflow com questions 12942365 detecting a pixelated image in python还有关于q
  • 数字求和的算法?

    我正在寻找一种数字求和的算法 让我概述一下基本原则 假设你有一个号码 18268 1 8 2 6 8 25 2 5 7 7 是我们的最终数字 它基本上是将整个数字中的每个数字相加 直到我们得到一个 也称为 核心 数字 它经常被命理学家使用
  • 计算给出数组中最小标准差的子集

    让我们有一个大小的向量N 例如 x rand N 1 我想计算长度子集的最小标准差K在向量中 When N and K很小 很容易找到最好的子集 因为我可以使用nchoosek N K 枚举所有可能的子集 但是当值N and K比我们说的要
  • O(n^2) 与 O (n(logn)^2)

    时间复杂度是O n 2 or O n logn 2 better 我知道当我们简化它时 它就变成了 O n vs O logn 2 and logn lt n 但是关于logn 2 n is only less than log n 2 f
  • 将数字 n 拆分为 k 个不同数字的总和

    我有一个数字 n 我必须将它分成 k 个数字 使得所有 k 个数字都是不同的 k 个数字的总和等于 n 并且 k 最大 例如 如果 n 为 9 则答案应为 1 2 6 如果 n 为 15 则答案应为 1 2 3 4 5 这就是我尝试过的 v
  • 使用 stl sort 对表进行排序

    我有一个巨大的表 约 50Gb 格式为 i j k 来自稀疏矩阵 存储为 uint32 t idx1 idx2 float vals uint32 t tablesize 我想使用给定的比较函数 即 idx1 和 idx2 的函数 对其进行
  • 随机排列

    我无法找到一种随机洗牌元素的好方法std vector经过一些操作后 恢复原来的顺序 我知道这应该是一个相当简单的算法 但我想我太累了 由于我被迫使用自定义随机数生成器类 我想我不能使用std random shuffle 无论如何这没有帮
  • java中的Anagram算法

    我想做字谜算法但是 这段代码不起作用 我的错在哪里 例如 des 和 sed 是字谜 但输出不是字谜 同时我必须使用字符串方法 不是数组 public static boolean isAnagram String s1 String s2
  • 埃拉托斯特尼筛法是生成 1 到 N 素数的最佳算法吗?

    我在一次采访中被问到这个问题 我使用埃拉托色尼筛子概念和数组实现了一种算法 有没有更好的方法来解决这个问题 对于不知道筛子的人 请点击以下链接 http en wikipedia org wiki Sieve of Eratosthenes
  • 仅使用两个变量交换两个数字

    它如何执行交换 a a b b a b a b a 我不同意把它换成书 书中的选项包括 a和b的值的补集 否定和b 希望这些选项也不能满足它 正确的算法应该是 a a b b a b a a b
  • 如何在大空间尺度上加速A*算法?

    From http ccl northwestern edu netlogo models community Astardemo http ccl northwestern edu netlogo models community Ast
  • 广度优先搜索:检查访问状态的时机

    在有向图的广度优先搜索中 可能循环 当一个节点出队时 其所有尚未访问的子节点都会入队 并且该过程将继续 直到队列为空 有一次 我以相反的方式实现它 将节点的所有子节点排队 并在节点出队时检查访问状态 如果正在出队的节点之前已被访问过 则该节
  • 创建将 n 个用户放入 k 个组的所有可能方法

    给定 n 个用户 u 1 u 2 u n 和 k 个组 g 1 g 2 g k 创建所有组的所有可能组合 基本上 最后每个组合都是一个Map 其中第一个Integer是用户ID 第二个Integer是组ID 例如 u 1 g 1 u 2 g
  • 找到一个恰好出现了 N/2 次的数字

    这是我的面试问题之一 给定一个包含 N 个元素的数组以及元素出现的位置正好 N 2次 其余 N 2 个元素是unique 您如何找到运行时间更好的元素 请记住 元素未排序 您可以假设 N 是偶数 例如 input array 10 2 3
  • 为每个英文单词生成唯一序列号的算法

    对于应用程序 我需要为每个英语单词生成唯一的序列号 最好的方法是什么 一个限制是序列号生成算法应该在普通台式计算机中非常有效 Thanks 你有所有可能的单词的列表吗 如果是 则从第一个字的 0 开始 每个字将序列号加 1 如果不是 那么保
  • 我该如何解决? KnapSack - 值完全相同,但每个对象都有三个权重

    我在解决我的练习时遇到问题 我读到了动态规划和算法 我认为我的练习是 特定背包问题 我用暴力法解决了它 但我无法用动态规划解决它 我有一艘重300吨的船 背包 有些晶体本身含有 3 种物质 X Y Z 每种物质都有重量 并且所有晶体都具有相
  • 沿着长数据序列在固定大小的移动窗口中查找中值

    给定一个数据序列 可能有重复项 一个固定大小的移动 窗口 从数据开始处每次迭代时移动窗口 序列 使得 1 从窗口中删除最旧的数据元素并添加新数据 元素被推入窗口 2 求每次移动时窗口内数据的中位数 以下帖子没有帮助 有效地找到随机序列的中值
  • 如何发现“贪婪”算法?

    我正在读一本关于 贪婪 算法 但我很难发现它们解决真正的 顶级程序员 问题 If I know给定的问题可以用 贪婪 算法来解决 因此编写解决方案非常容易 然而 如果我没有被告知这个问题是 贪婪的 我就无法发现它 用 贪婪 算法解决的问题有
  • 数字总和直到作为输入给出的数字

    如果给出一个数字作为输入 则找到该数字之前所有数字的总和 例如输入 11 则答案为 1 2 9 1 0 1 1 蛮力方法是计算所有小于某个数字的数字的数字之和 我已经实现了该方法 我想知道是否有其他方法可以在不实际计算每个数字的数字之和的情

随机推荐

  • 什么时候使用Jersey的@Singleton注解?

    我正在开发 RESTful Web 服务 并在阅读泽西岛时文档 https jersey java net documentation latest jaxrs resources html d0e1851我发现了一个注释 Singleto
  • 访问 GCP 深度学习平台映像的 dockerfiles

    我正在使用源自的图像深度学习容器 https cloud google com ai platform deep learning containers docs 在 AI Platform Notebooks 产品中运行多个任务 为了对我
  • C# Newtonsoft 反序列化 JSON 数组

    我正在尝试使用 Newtonsoft 反序列化数组 以便我可以在列表框中显示来自基于云的服务器的文件 但无论我尝试什么 我总是会收到此错误 Newtonsoft Json JsonReaderException 解析值时遇到意外字符 路径
  • 将Optional.absent()值简洁地传递给方法

    使用Guava的一个问题Optional类型作为方法的参数是你不能简单地写 method declaration public void foo Optional
  • 使用 json 向 RESTful WCF 发送 Post 请求

    我已经尝试了每种组合来发送请求 以从 jQuery 向 RESTful WCF 发送 POST 请求 有人可以模仿并使其发挥作用吗 代码在这里 http pastebin com Ua97919C http pastebin com Ua9
  • 如何使用meta-toolchain-qt5构建Qt(带有QtWebEngine支持)?

    我正在尝试使用构建 Qtmeta toolchain qt5 但是当我通过这样做时poky glibc x86 64 meta toolchain qt5 cortexa7hf vfp vfpv4 neon toolchain 2 0 1
  • 设置 Angular 2 http 请求的基本 url

    我正在尝试为所有 Angular 2 http 请求设置基本 url 以下是我的应用程序的基本设置 class HttpOptions extends BaseRequestOptions url string http 10 7 18 2
  • 数据聚合和缓存:如何按时间间隔快速绘制大型时间序列数据集的图表

    我有一个巨大的时间序列数据集 我想绘制图表 时间序列可以追溯到 5 年前 从后端的角度来看 以各种分辨率 间隔 显示这些数据的常用方法是什么 本质上我想绘制这样的数据图表 https bitcoinwisdom com markets bi
  • CSS: *html #id_name

    我有这个代码 html alertBox height 100px modalContainer gt alertBox position fixed 这是由CSS支持的吗 我在其他网站上找到了这段代码 我忘记了链接 html alertB
  • 如何将组合框放置在选项卡的标题中?

    是否可以在选项卡标题中显示组合框 如果是 extjs 组合则更好 我创造了jsfiddle 上的一个例子 http jsfiddle net andron v4ZzT 但存在这样的问题 1 无法显示Combo的选项列表 鼠标点击不起作用 2
  • Ruby 中的 DateTime.parse() 是否依赖于语言环境?

    我想知道以下示例的输出 解析时01 03 它会被解决为Mar 1st or Jan 3rd Ruby 不依赖于语言环境 因为红宝石是一个服务器端语言而不是客户端像 JavaScript 一样的语言 Ruby 使用系统时钟yourWeb 应用
  • 单例属性

    好吧 如果我创建一个单例类并通过公共静态属性公开单例对象 我明白了 但我的单例类还有其他属性 这些应该是静态的吗 这些也应该是私人的吗 我只想通过执行以下操作来访问单例类的所有属性 MySingletonClass SingletonPro
  • 是否可以修改 PDF 表单字段名称?

    情况是这样的 我有一个 PDF 其中包含自动生成的 pdf 表单字段名称 问题是这些名称不太用户友好 它们看起来像 topmostSubform 0 Page1 0 Website Address 0 我希望能够更改它们 使它们类似于 We
  • 时间:2019-03-17 标签:c#backgroundworker和partialclass

    我在实现从堆栈溢出获得的代码时遇到问题 它是关于终止后台工作进程的 我的代码如下 using System using System Collections Generic using System Data using System Dr
  • 从 jar 加载 .so 文件

    我用 C 语言创建了一个库 并使用 JNI 从 Java 调用它 因此我有我的包和一个包含 libMYLIB so 文件的 lib 文件夹 我记得在Java写作中 static System loadLibrary MYLIB 如果我使用选
  • 如何判断何时创建新组件?

    我一直在寻找背后的逻辑当有人在 AngularJS Angular 上的 Web 应用程序中创建新组件时但我认为这更通用 可能适用于所有基于组件的前端框架 我知道有像这样的一些原则应该是抽象的和可重用的但例如我在角度文档中看到 每个单独的路
  • 使用 protobuf-net 序列化继承的类

    我在使用 protobuf net 序列化派生类时遇到问题 我不知道是因为不支持 还是我做错了什么 我有一个通用基类 我可以直接序列化 然后我对其进行专门化 但我无法序列化这个基类 以下是两个类的代码和使用示例 难道我做错了什么 Edit
  • CckEditor - 从 AJAX 加载的模板

    我正在使用 CkEditor 并且想要定义一个自定义模板 该模板使用 AJAX 函数来加载 HTML 字符串 我已经能够定义自定义模板 但如果我对模板对象的 html 属性使用函数 则该函数永远不会执行 是否可以使用 AJAX 和默认模板插
  • 以编程方式插入行(父行和子行)

    我正在使用 Spring 和 JDBCTemplate 该场景是 CUSTOMER 表和 ORDERS 表的父子关系 我想做一个插入 例如 1 个客户和 5 个订单 但我不确定如何以编程方式在 CUSTOMER 表中插入一行 如何获取 Or
  • 实施二分查找有哪些陷阱? [关闭]

    Closed 这个问题需要多问focused help closed questions 目前不接受答案 二分查找比看起来更难实现 虽然二分搜索的基本思想相对简单 但细节可能出人意料地棘手 Donald Knuth 新的二分搜索实现中最有可