在二分搜索中计算中间值

Posted

技术标签:

【中文标题】在二分搜索中计算中间值【英文标题】:Calculating mid in binary search 【发布时间】:2011-10-07 19:03:49 【问题描述】:

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

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替换。”

我看不出这会如何导致溢出。当我在脑海中为几个不同的输入运行算法时,我看不到中间值超出数组索引。

那么,什么情况下会发生溢出呢?

【问题讨论】:

2个数的加减乘乘都会产生更多的位,所以显然有溢出的可能 binary search middle value calculation的可能重复 【参考方案1】:

post 详细介绍了这个著名的错误。正如其他人所说,这是一个溢出问题。链接上推荐的修复方法如下:

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_VALUEInteger.MAX_VALUE 范围执行二分搜索。

【讨论】:

您提供的链接对问题有明确的解释。谢谢! 只使用 (high / 2 + low / 2) 可以吗? 为什么 (low + high) 在上述替代方法中,即 int mid = (low + high) >>> 1 不会导致溢出? 这个溢出错误是否也适用于python? Python 具有任意精度整数,因此添加长整数不会导致问题。 @Fakrudeen (high / 2 + low / 2) 截断最低有效位并会产生不正确的结果。例如,low=3, high=5mid 变为 3 而应该为 4。【参考方案2】:

以下 C++ 程序可以向您展示 32 位无符号整数如何发生溢出:

#include <iostream>
using namespace std;

int main ()

  unsigned int  low = 33,  
                high = 4294967290, 
                mid;

  cout << "The value of low is " << low << endl;
  cout << "The value of high is " << high << endl;

  mid = (low + high) / 2;

  cout << "The value of mid is " << mid << endl;
  
  return 0;

如果你在 Mac 上运行它:

$ g++ try.cpp; ./a.out
The value of low is 33
The value of high is 4294967290
The value of mid is 13

mid 的值可能预期为2147483661,但low + high 溢出,因为 32 位无符号整数不能包含正确的值,并返回 27,因此 mid 变为 @ 987654329@.

mid的计算改为

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

然后会显示

The value of mid is 2147483661

简单的答案是,加法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

就像,如果我们看两棵树,一棵高 200 英尺,一棵高 210 英尺,“中点”或“均值”是什么?我们不必先将它们加在一起。我们可以看出差值是 10 英尺,我们可以将其中的一半(即 5)加到 200 中,我们知道它是 205 英尺。

为了将其纳入历史观点,Robert Sedgewick 提到,第一次二分搜索是在 1946 年提出的,直到 1964 年才正确。Jon Bentley 在他 1988 年的《Programming Pearls》一书中描述了超过 90% 的专业人士几个小时后,程序员无法正确编写它。但即使是 Jon Bentley 本人也有 20 年的溢出错误。 1988 年发表的一项研究表明,在 20 部教科书中只有 5 部能找到准确的二分搜索代码。 2006 年,Joshua Bloch 写了一篇关于计算 mid 值的错误的博客文章。所以这个代码花了 60 年的时间才正确。但是现在,下次面试的时候,记得在那5分钟内写好。

【讨论】:

我认为您的意思是std::int32_t,而不是int(它的范围可能比您预期的要大)。【参考方案3】:

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

【讨论】:

【参考方案4】:

Jeff 建议非常好的 post 阅读有关此错误的信息,如果您想快速了解这里是摘要。

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

【讨论】:

【参考方案5】:

这是一个示例,假设您有一个非常大的数组,大小为 2,000,000,00010 (10^9 + 10),左侧的 index 位于 2,000,000,000,右侧的 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,你会得到integer overflow

但是low + ((high - low) / 2) 可以。 2,000,000,000 + ((2,000,000,001 - 2,000,000,000) / 2) = 2,000,000,000

【讨论】:

【参考方案6】:

潜在的溢出存在于l+u 添加本身中。

这实际上是 JDK 中二分查找的 a bug in early versions。

【讨论】:

链接失效 @jdhao - 当时它正在工作。接受的答案有一个完整帐户的链接,由错误代码的作者提供。无论如何,我已经更新了我的链接。【参考方案7】:

其实下面计算mid的语句可能会导致INT range溢出。

mid = (start + end) /2

假设给定的有序输入列表非常大,并假设它超过了INT range(-2^31 to 2^31-1)start + end 可能会导致异常。为了解决这个问题,写了以下语句:

mid = start + (end-start)/2

最终它会产生相同的表达式。但是这个技巧避免了异常。

【讨论】:

【参考方案8】:

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

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

但是,问题已解决为:-

int mid=l+(h-l)/2; 位操作:为了更快的计算->int mid=((unsigned int)l+(unsigned int)h) &gt;&gt; 1 ;

其中>>是右移运算符。

希望这会有所帮助:)

【讨论】:

【参考方案9】:

我用一个会发生数字溢出的例子制作了这个视频。

https://youtu.be/fMgenZq7qls

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

【讨论】:

【参考方案10】:

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

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

【讨论】:

【参考方案11】:

这是一个非常微妙的错误,第一次很容易错过。网上的大部分文章似乎都没有清楚地解释这个错误是如何发生的,以及优化后的公式是如何防止溢出的。

经过大量挖掘,我找到了this 文章,该文章对使用mid = (left+right)/2 公式时如何发生错误以及如何使用mid = low + ((high - low) / 2) 克服该错误进行了出色而详细的解释。最重要的是他们用例子来解释它,这使得理解变得更加容易。

这也解释了为什么mid = low + ((high - low) / 2) 不会导致溢出。

【讨论】:

以上是关于在二分搜索中计算中间值的主要内容,如果未能解决你的问题,请参考以下文章

二分搜索

JAVA 二分搜索法

二分查找

二分搜索算法

算法复习笔记:二分查找

查找算法-二分查找