我正在阅读一本算法书,其中包含以下二进制搜索算法:
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的值超出数组索引。
那么,在哪种情况下会发生溢出?
答案 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,000
和10 (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)
我已经创建了这个视频,并举例说明了数字溢出的情况。
通常,对于需要从数组中查找元素的简单二进制搜索,由于Java等语言中数组大小的限制,这种情况不会发生,但是问题空间不仅限于数组,则可能会发生此问题。请观看我的视频以获取实际示例。
答案 8 :(得分:0)
为了避免溢出,您还可以这样做: int midIndex = (int) (startIndex/2.0 + endIndex / 2.0);
您将两个指数除以 2.0 -> 你得到两个小于或等于 Integer.MAX_VALUE / 2 的双精度值,它们的总和也小于或等于 Integer.MAXVALUE 和一个双精度值。 Integer.MIN_VALUE 也是如此。最后,您将总和转换为 int 并防止溢出;)