在二进制搜索中计算mid

时间:2011-07-18 15:22:17

标签: algorithm binary-search

我正在阅读一本算法书,其中包含以下二进制搜索算法:

public class BinSearch {
  static int search ( int [ ] A, int K ) {
    int l = 0 ;
    int u = A. length −1;
    int m;
    while (l <= u ) {
      m = (l+u) /2;
      if (A[m] < K) {
        l = m + 1 ;
      } else if (A[m] == K) {
        return m;
        } else {
          u = m−1;
        }
       }
       return −1;
      }
 }

作者说“错误在作业m = (l+u)/2;中可能导致溢出,应由m = l + (u-l)/2替换。”

我看不出那会导致溢出。当我在脑海中运行算法以获得一些不同的输入时,我没有看到mid的值超出数组索引。

那么,在哪种情况下会发生溢出?

9 个答案:

答案 0 :(得分:40)

这个post详细介绍了这个着名的bug。正如其他人所说,这是一个溢出问题。链接上建议的修复方法如下:

int mid = low + ((high - low) / 2);

// Alternatively
int mid = (low + high) >>> 1;

也许值得一提的是,如果允许使用负数索引,或者它甚至不是正在搜索的数组(例如,搜索满足某些条件的某个整数范围内的值),则上面的代码可能不是也是正确的。在这种情况下,像

一样丑陋
(low < 0 && high > 0) ? (low + high) / 2 : low + (high - low) / 2

可能是必要的。一个很好的例子是searching for the median in an unsorted array without modifying it or using additional space,只需在整个Integer.MIN_VALUE - Integer.MAX_VALUE范围内执行二进制搜索。

答案 1 :(得分:4)

问题是首先评估(l+u),并且可能溢出int,因此(l+u)/2将返回错误的值。

答案 2 :(得分:3)

杰夫建议真的很好post阅读有关此错误的内容,如果您想快速了解,请参阅摘要。

在编程中Pearls Bentley说类似的行“将m设置为l和u的平均值,截断为最接近的整数”。从表面上看,这个断言可能看起来是正确的,但是对于低和高的int变量的大值都失败了。具体而言,如果low和high的总和大于最大正int值(2 ^ 31-1),则失败。总和溢出为负值,当除以2时,该值保持为负。在C中,这会导致数组索引超出界限且结果不可预测。在Java中,它会抛出ArrayIndexOutOfBoundsException。

答案 3 :(得分:2)

潜在的溢出是l+u添加本身。

这实际上是JDK中二进制搜索的a bug in early versions

答案 4 :(得分:2)

这里是一个示例,假设您有一个很大的数组,大小为2,000,000,00010 (10^9 + 10),而左边的index位于2,000,000,000,右边的{{1} }位于index

使用2,000,000,000 + 1总计为lo + hi。由于2,000,000,000 + 2,000,000,001 = 4,000,000,001的最大值为integer。因此,您不会得到2,147,483,647,您将得到4,000,000,000 + 1

答案 5 :(得分:1)

int mid=(l+h)/2;会导致整数溢出问题。

(l + u)被评估为一个较大的负整数值及其一半 返回。现在,如果我们在数组中搜索元素, 会导致“索引超出范围错误”。

但是,此问题已解决为:-

  • int mid=l+(h-l)/2;
  • 位操作:用于更快的计算-> int mid=((unsigned int)l+(unsigned int)h) >> 1 ;

其中>>是右移运算符。

希望这会有所帮助:)

答案 6 :(得分:0)

简单的答案是,附加l + u可能溢出,并且在某些语言中具有不确定的行为,如a blog post by Joshua Bloch, about a bug in the Java library for the implementation of binary search中所述。

某些读者可能不了解它的含义:

l + (u - l) / 2

请注意,在某些代码中,变量名称是不同的,并且是

low + (high - low) / 2

答案是:假设您有两个数字:200和210,现在需要“中间数字”。假设如果将两个数相加,结果大于255,则可能会溢出,并且行为未定义,那么您该怎么办?一种简单的方法是将它们之间的差值加起来,但只将其一半加到较小的值上:看一下200和210之间的差值。它是10。(您可以将其视为“差值”或“长度” “, 它们之间)。因此,您只需要将10 / 2 = 5加到200,得到205即可。您无需先将200和210加在一起,这就是我们得出计算结果的方式:(u - l)是区别。 (u - l) / 2是其中的一半。将其添加到l,我们有l + (u - l) / 2

从历史角度来看,罗伯特·塞奇威克(Robert Sedgewick)提到,第一次二进制搜索是在1946年提出的,直到1964年才是正确的。乔恩·本特利(Jon Bentley)在1988年的《 Programming Pearls》一书中描述了超过90%几个小时后,程序员无法正确编写它。但是,即使乔恩·本特利本人也有20年的溢出漏洞。 1988年发表的一项研究表明,仅在20本教科书中的5本中找到了准确的二进制搜索代码。 2006年,约书亚·布洛赫(Joshua Bloch)撰写了该博客文章,内容涉及计算mid值的错误。因此,此代码的正确性花了60年。但是现在,下次要进行面试时,请记住在20分钟内正确写完。

答案 7 :(得分:0)

我已经创建了这个视频,并举例说明了数字溢出的情况。

https://youtu.be/fMgenZq7qls

通常,对于需要从数组中查找元素的简单二进制搜索,由于Java等语言中数组大小的限制,这种情况不会发生,但是问题空间不仅限于数组,则可能会发生此问题。请观看我的视频以获取实际示例。

答案 8 :(得分:0)

为了避免溢出,您还可以这样做: int midIndex = (int) (startIndex/2.0 + endIndex / 2.0);

您将两个指数除以 2.0 -> 你得到两个小于或等于 Integer.MAX_VALUE / 2 的双精度值,它们的总和也小于或等于 Integer.MAXVALUE 和一个双精度值。 Integer.MIN_VALUE 也是如此。最后,您将总和转换为 int 并防止溢出;)