在二分搜索中计算中间值
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_VALUE
–Integer.MAX_VALUE
范围执行二分搜索。
【讨论】:
您提供的链接对问题有明确的解释。谢谢! 只使用 (high / 2 + low / 2) 可以吗? 为什么 (low + high) 在上述替代方法中,即 int mid = (low + high) >>> 1 不会导致溢出? 这个溢出错误是否也适用于python? Python 具有任意精度整数,因此添加长整数不会导致问题。 @Fakrudeen(high / 2 + low / 2)
截断最低有效位并会产生不正确的结果。例如,low=3, high=5
、mid
变为 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,000
和 10 (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) >> 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)
不会导致溢出。
【讨论】:
以上是关于在二分搜索中计算中间值的主要内容,如果未能解决你的问题,请参考以下文章