引言
二分查找的过程是不断把搜索边界缩小,往中间“挤”出结果的过程。
二分查找难点
假设
l
l
l是当前搜索区间左边界,
r
r
r是当前搜索区间右边界,
m
m
m是向下取整中位点,
A
A
A为有序表。
之后示例中
l
0
l_0
l0和
r
0
r_0
r0为初始边界,
v
v
v为目标值
- 循环条件: l < r o r l ≤ r l < r \ or \ l \le r l<r or l≤r
- 边界调整: l = m o r l = m + 1 ; r = m o r r = m − 1 l = m \ or \ l = m + 1; r = m \ or \ r=m-1 l=m or l=m+1;r=m or r=m−1
- 返回结果: l o r l + 1 o r r o r r + 1 l \ or \ l+1 \ or \ r \ or \ r+1 l or l+1 or r or r+1
以上问题可以通过取左闭右开的搜索区间来尝试解决,最终结论如下
- 循环条件: l < r l<r l<r
- 边界调整: l = m + 1 , r = m l = m + 1, r = m l=m+1,r=m
- 返回结果: l o r r l \ or \ r l or r
个人感性认识上,正是因为二分除法中,中位数有两种取法,而左闭右开选择了向下取整的中位数,来减少讨论。
C++STL库实现的二分查找函数,提供的参数也是相同的思路
lower_bound(first,last,value)
:从下标[first, last)
内,二分查找第一个大于或等于value的数字
upper_bound(first,last,value)
:从下标[first, last)
内,二分查找第一个大于value的数字
lower_bound的相同效果实现思路
// 求 第一个 x >= v
while (l < r) {
m = l + ((r - l) >> 1); // 防溢
if (A[m] < v) l = m + 1;
else r = m;
}
return l; // 或 return r;
循环条件
在查找过程中循环继续的条件
- [ l , r ) [l,r) [l,r)不为空,需要继续搜索
- 此时 [ l 0 , l ) [l_0,l) [l0,l)所有元素小于 v v v
- 此时 [ r , r 0 ) [r,r_0) [r,r0)所有元素大于等于 v v v
边界调整
- A [ m ] < v A[m]<v A[m]<v, m m m应该被划分在 [ l 0 , l ) [l_0,l) [l0,l)内,应调整 l = m + 1 l = m + 1 l=m+1
- A [ m ] ≥ v A[m] \ge v A[m]≥v, m m m应该被划分在 [ r , r 0 ) [r,r_0) [r,r0)内,应调整 r = m r = m r=m
返回结果
最终循环结束时,结果为第一个大于等于 v v v的元素下标
- [ l , r ) [l,r) [l,r)为空, l = r l = r l=r
- [ l 0 , l ) [l_0,l) [l0,l)所有元素小于 v v v
- [ r , r 0 ) [r,r_0) [r,r0)所有元素大于等于 v v v
upper_bound的相同效果实现思路
// 求 第一个 x > v
while (l < r) {
m = l + ((r - l) >> 1);
if (A[m] <= v) l = m + 1; // 差别在此,A[m] == v 的归属
else r = m;
}
return l; // 或 return r;
循环条件
在查找过程中循环继续的条件
- [ l , r ) [l,r) [l,r)不为空,需要继续搜索
- 此时 [ l 0 , l ) [l_0,l) [l0,l)所有元素小于等于 v v v
- 此时 [ r , r 0 ) [r,r_0) [r,r0)所有元素大于 v v v
在查找过程中循环继续的条件
边界调整
- A [ m ] ≤ v A[m]\le v A[m]≤v, m m m应该被划分在 [ l 0 , l ) [l_0,l) [l0,l)内,应调整 l = m + 1 l = m + 1 l=m+1
- A [ m ] > v A[m] > v A[m]>v, m m m应该被划分在 [ r , r 0 ) [r,r_0) [r,r0)内,应调整 r = m r = m r=m
返回结果
最终循环结束时,结果为第一个大于 v v v的元素下标
- [ l , r ) [l,r) [l,r)为空, l = r l = r l=r
- [ l 0 , l ) [l_0,l) [l0,l)所有元素小于等于 v v v
- [ r , r 0 ) [r,r_0) [r,r0)所有元素大于 v v v
其他
可以用lower_bound
和upper_bound
解决常用二分查找问题如下
- 小于
v
v
v的上界
lower_bound(l,r,v)
- 1 - 小于等于
v
v
v的上界
upper_bound(l,r,v)
- 1 - 大于等于
v
v
v的下界
lower_bound(l,r,v)
- 大于
v
v
v的下界
upper_bound(l,r,v)
中间两个较为常用
例
int searchInsert(int* nums, int numsSize, int target)
{
int l, r;
l = 0, r = numsSize;
while (l < r) {
int m = l + ((r - l) >> 1);
if (nums[m] < target) l = m + 1;
else r = m;
}
return l;
}
LettCode 34.在排序数组中查找元素的第一个和最后一个位置
int* searchRange(int* nums, int numsSize, int target, int* returnSize)
{
int l = 0, r = numsSize, m;
int* ret = malloc(sizeof(int) * 2);
*returnSize = 2;
ret[0] = ret[1] = -1;
if (numsSize == 0) return ret;
while(l < r) {
m = l + ((r - l) >> 1);
if (nums[m] < target) l = m + 1;
else r = m;
}
if (r != numsSize && nums[l] == target) ret[0] = l;
l = 0, r = numsSize;
while (l < r) {
m = l + ((r - l) >> 1);
if (nums[m] <= target) l = m + 1;
else r = m;
}
if (l != 0 && nums[l - 1] == target) ret[1] = l - 1;
return ret;
}