细说二分查找
细说以下二分查找中的那些坑。
二分查找是一个听起来很简单的算法,但是实际上这个算法有很多坑,想快速写出百分百正确的二分查找并不容易。
Donald Knuth 在其著作 The Art of Computer Programming, Volume 3: Sorting and Searching 中提到,“虽然第一篇二分搜索的论文在1946年就发表了,但是第一个没有错误的二分搜索程序却直到1962年才出现。”
然而,实际上,1962年出现的一些二分搜索算法,现在看来依然有bug,请往下看。
曾经的 Java 类库中的 bug
当年 Java 类库中的二分搜索算法也有 bug,当时的算法没有考虑整形相加可能的溢出问题。当时的代码如下:
1 | public static int binarySearch(int[] a, int key) { |
其中,low + high
是有可能溢出的,如果要解决这个问题,可以把计算 mid
的这句改为:
1 | int mid = (low + high) >>> 1 |
之所以可以改为这样,并不是因为对所有的数字计算平均值都可以采用这种方法,只是这种写法在二分搜索这个算法中是正确的。
>>>
移位运算符会把符号位一起移动。int 类型的值是区分正负的,一个 int 型变量的二进制形式的首位为 0,代表这个数是正数,为1代表这个数是负数。如果两个数相加发生了溢出,那么就相当于符号位后边的各位相加之后向符号位发生了进位,两个正数相加,但是符号为却变成了1,结果变成了一个负数,这就是出错的情况。但是如果我们使用 >>>
运算符,相当于不考虑符号位的存在了,把二进制的第一位也当成正常存储值的位,这样两个数相加,直接右移一位,原本会溢出的结果,会得到正确的答案。
但是如果有两个负数,两数相加之后有可能会溢出下限,使用 >>>
运算符并不行。
之所以在二分算法中可以这么做,是因为我们是对数组的下标进行计算,而下标只能是正数,不能是负数。
当年写出这个 Bug 的作者 Joshua Bloch 亲自写文章讲这件事:Extra, Extra - Read All About It: Nearly All Binary Searches and Mergesorts are Broken - Joshua Bloch
STL 中的 lower_bound 和 upper_bound
两个函数其实都是求“下界”。
lower_bound()
是求区间内第一个大于等于某个值的元素的索引。
upper_bound()
是求区间内第一个大于某个值的元素的索引。
两个函数的实现极其相似,lower_bound()
中的判断部分是 comp(*it, value)
,而 upper_bound()
中是 !comp(*it, value)
。相当于第一个是 if(array[mid] < value)
,第二个是 if(!(array[mid] > value))
。
参考:
std::lower_bound - cppreference.com
std::upper_bound - cppreference.com
参考资料
- 强烈推荐:二分查找有几种写法?它们的区别是什么? - 知乎
- 用Java实现C++::std中的upper_bound和lower_bound
- 二分查找–那个隐藏了10年的Java Bug
- 位运算相关 - 52Heartz’s Blog
- 计算两个数的平均数 - 52Heartz’s Blog
- 编程珠玑. 第2版. 2008.10. 人民邮电出版社. 第4章. 编写正确的程序.
- 【二分查找法】你真的写对了吗?
- Extra, Extra - Read All About It: Nearly All Binary Searches and Mergesorts are Broken - Joshua Bloch
- 拓展:【编程珠玑】第二章 二分查找的巧妙应用