一般来说,二分查找在学校里都会教过。但是写一个bug free的二分查找真的那么容易么,曾经有人统计过 就算是CMU的博士生写出来的二分查找也大多存在着各种各样的问题。
1. 简单的二分查找竟然有这么多坑!
// 最简单的二分查找
int binarySearch(vector<int>& nums, int target)
{
int l = 0, r = nums.size()-1; // 1. 坑1
while(l<=r){
int mid = l + (r-l)/2; // 2. 坑2
if(nums[mid] == target){
return mid;
}else if(nums[mid] < target){
l = mid + 1;
}else if(nums[mid > target){
r = mid - 1;
}
}
return r; // 3. 坑3
}
上面一段简单的二分查找存在着3个坑,
坑1: 最右侧 r 是 nums.size() 和 nums.size()-1, 傻傻分不清。这涉及到二分的一些寻址规则,到底是前闭后闭,还是前闭后开。
坑2: m i d = l + ( r − l ) / 2 mid = l + (r-l)/2 mid=l+(r−l)/2 还是 m i d = ( l + r ) / 2 mid = (l +r)/2 mid=(l+r)/2。注意一些标准库也是直接用 m i d = ( l + r ) / 2 mid = (l +r)/2 mid=(l+r)/2, 这是非常错误的写法。当数据量特别大的时候, (l +r) 如果会出现溢出。举个例子 l,r 如果声明是整型, 那么 l+r 很有可能超过 int的最大值,从而出现溢出。
**坑3:**返回到底是l 和 r, 一般来说,对于数组没有重复值的时候,返回哪个都没关系。但是如果有重复值的时候,会涉及到返回target第一次出现的坐标,或者target最后一次出现的坐标。
2. 关于数组中出现重复数值的二分查找
C++标准库中提供了 lower_bound 和 upper_bound 的写法
- lower bound是指找到 >=目标数的第一个数
- upper bound是指找到 > 目标数的第一个数
2.1 前闭后开的写法 [low, high)
这种写法也是图灵将获得者Dijsktra老前辈所推崇的写法。
下面提供标准的写法
// lower bound + 前闭后开
// 找到第一次出现目标数的位置,否则返回end
int lower_bound(vector<int>&nums, int target){
int low = 0, high = nums.size();
while(low < high){
int mid = low + (high-low) / 2;
if(nums[mid] >= target){
high = mid;
}else{
low = mid+1;
}
}
return low;
}
// upper bound + 前闭后开
// 找到第一次大于目标数的位置,否则返回end
int upper_bound(vector<int>&nums, int target){
low = 0, high = nums.size();
while(low < high){
int mid = low + (high-low) / 2;
if(nums[mid] > target){
high = mid;
}else{
low = mid+1;
}
}
return high;
}
注意: 上面写法是前闭后开 [low, high) 的写法。
有几个点需要注意:
- high的初始化赋值是从 开区间 开始。
- ±1的位置只出现一次: 只更新low = mid+1,更新high = mid
- 最重要的一点,上述写法最终结束的时候,low=high, 无需纠结返回哪个
- upper_bound终止条件是num[mid] > target的时候,因此返回的是第一个大于target的位置。
2.2 前闭后闭的写法 [low, high]
// lower bound + 前闭后闭
// 找到第一次出现目标数的位置,否则返回end
int lower_bound(vector<int>&nums, int target){
int low = 0, high = nums.size()-1;
while(low <= high){
int mid = low + (high-low) / 2;
if(nums[mid] >= target){
high = mid-1;
}else{
low = mid+1;
}
}
return low;
}
// upper bound + 前闭后闭
// 找到第一次大于目标数的位置,否则返回end
int upper_bound(vector<int>&nums, int target){
int low = 0, high = nums.size()-1;
while(low <= high){
int mid = low + (high-low) / 2;
if(nums[mid] > target){
high = mid-1;
}else{
low = mid+1;
}
}
return low;
}
这种写法则需要注意返回的是low和high
- 观察while终止条件,low在大于high的时候进行终止,因此最终low = high+1
- 观察判断时候的等号,等号是 查找到target的最后进入条件,但是进入后,low和high会进行更新。
2.3STL 源码中的lower bound写法
template<typename _ForwardIterator, typename _Tp, typename _Compare>
_ForwardIterator
__lower_bound(_ForwardIterator __first, _ForwardIterator __last,
const _Tp& __val, _Compare __comp)
{
typedef typename iterator_traits<_ForwardIterator>::difference_type
_DistanceType;
_DistanceType __len = std::distance(__first, __last);
while (__len > 0)
{
_DistanceType __half = __len >> 1;
_ForwardIterator __middle = __first;
std::advance(__middle, __half);
if (__comp(__middle, __val))
{
__first = __middle;
++__first;
__len = __len - __half - 1;
}
else
__len = __half;
}
return __first;
}
这个看起来就比较复杂了。
哈哈,这就跟写茴香豆的茴字一样,掌握适合自己的写法才是最好的。
不过本质上这种写法也是前闭后开式写法。