二分查找,就是在一个区间内查找某个值(eg: target)的时候,每次把区间一分为二,然后选择满足性质的区间(即包含 target)继续一分为二,直到区间长度为一。
最开始刷二分题的时候,比较慢,可能自己没整理好的一个通用的思路,自从用了 y总
的模板后,就离不开了,真的很爽。
模板
1 | bool check(int x) {/* ... */} // 检查x是否满足某种性质 |
其实,上面模板的代码分别对应两种情况
- bsearch_1: 找左边界,不断的向左逼近,比如:求 >=x 的最小值
- bsearch_2: 找右边界,不断的向右逼近,比如:求 <=x 的最大值
画个图,来加深下理解;
上图中,红色的区间是满足性质的,反之绿色的则不满足性质,而模板中的 check(mid)
函数就是用来判断是否满足性质,从而将区间一分为二。
- bsearch_1
- true: 满足性质,区间变成:[l, mid], 不断的向左逼近,找左边界
- false: 不足,区间变成:[mid+1, r]
- bsearch_2
- true: [mid, r], 不断的向右逼近,找右边界
- false: [l, mid-1]
注意点
- 循环退出条件是
while(l < r)
, 不是while(l <= r)
;因为当while(l < r)
时,当条件是l >= r
就会退出,而这里则是l == r
; check(mid)
是用来判断mid
是否满足某种性质的,并且mid
是在所满足性质区间内的;bsearch_2
里面的mid = l + r + 1 >> 1;
,对比bsearch_1
为什么要加+1
呢?因为C++
是下取整的,当l = r-1
时,mid = l+r>>1 = r+r-1>>1 = r-1=l
,当满足性质时,l = mid = l
,此时l
的值没发生变化,这样就发生死循环了
解题思路
解题思路总结:
- 先画图,确定二分的边界(找左边界还是右边界)
- 根据边界来设计一个
check
函数(满足某种性质) - 判断区间怎么更新,决定是
l = mid
还是r = mid
练习
刷几道 lc 来找下感觉,巩固下模板知识。
704. 二分查找
一道很基础的题目,在升序数组里面找一个数,这里即可以找左边界,也可以找右边界,按上面的解题思路总结来考虑一下这道题。
- 左边界(bsearch_1)
- check 函数:
nums[mid] >= target
,则不断的向左逼近 - 因为是找左边界,所以是
r = mid
;
- check 函数:
- 右边界(bsearch_2)
- check 函数:
nums[mid] <= target
,则不断的向左逼近(跟左边界的代码是相对的) - 因为是找右边界,所以是
l = mid
;
- check 函数:
bsearch_1
找左边界,不断的向左逼近,找 >= target
的最小值。
1 | class Solution { |
bsearch_2
找右边界,不断的向右逼近,找 <= target
的最大值。
1 | class Solution { |
34. 在排序数组中查找元素的第一个和最后一个位置
这题跟上题是一样的思路,套用模板直接求第一个位置和最后一个位置的数即可。
- 第一个位置:见 704. 二分查找 - bsearch_1;
- 最后一个位置:见 704. 二分查找 - bsearch_2;
33. 搜索旋转排序数组
这道题目的数据并不是单调的,但也是可以用二分的,我们需要发现其中的性质。
发现上面的数据是有二段性的,即两个区间是有序的,如果我们能确定 target
在某一个区间,那么就可以在这个区间里面用二分了。
那怎么确定 target
在哪一个区间呢?这个简单,由于数组没有重复元素,第一段的最小值肯定大于第二段的最大值,我们只需要用 target
跟第二段的最大值(即数组最后一个元素)作比较即可。
那我们怎么判断找到这两个区间呢?也就是找到一个位置,把数组分成两个区间,可以遍历一次,如果 nums[i+1]
小于 nums[i]
,则说明 [0, i]
属于第一段,而 [i+1, n-1]
属于第二段,是否能用二分找到这个区间呢?也就是找到这个区间的最大值或者最小值,然后根据这里两个极值把数组分成两个区间。
我们先以求最大值为例。
最大值
按上面的解题思路总结来考虑一下这道题。
- 边界在哪?求最大值,有因为是递增的,肯定在右边
check
函数?>= nums[0]
就行,不断向右逼近- 区间更新?向右逼近,肯定是
l = mid;
好,开干。
1 | class Solution { |
最小值
解题思路同上。
1 | class Solution { |
用这里找最小值的思路就能解决 153. 寻找旋转排序数组中的最小值 了。
81. 搜索旋转排序数组 II
跟 33 的区别在于:这道题有重复数,那这个思路还行的同吗?
通过 33 的图,并结合这里的题意可知,只有第一个区间前面的数和第二个区间后面的数会重复,而这里题意是 编写一个函数来判断给定的目标值是否存在于数组中。
,所以我们把重复数给移除掉的话,是不影响结果的,这样就能用 33 的思路来解决了,具体思路见代码。
1 | class Solution { |
用这种去重的思路,同样能解决 154. 寻找旋转排序数组中的最小值 II。
69. x 的平方根
- 确定边界?这里需要返回一个数开平方根后的整数,如果一个数开平方根后的值
result
恰好是整数,那就直接返回result
;如果是小数呢?结合题目的示例 2
可知,应该返回<= result
的最大整数,所以这里是找右边界; check
函数?mid <= x/mid
- 区间更新?向右逼近,肯定是
l = mid;
1 | class Solution { |
思考
Q: 找左边界可以吗?
A: 当然是可以的,不过逻辑稍微会复杂点,因为 C++ 里除法是下取整,所以划分的区间是 [0, [mid][下取整]]
和 [[mid][下取整]+1, x]
,所以最后得到答案是 [mid][下取整]+1
,所以需要做 -1 操作。
1 | class Solution { |
还是上一个做法比较简单,符合常规思考逻辑。
总结
- 按照 解题思路 多做题目,熟能生巧;
- 有些题目二分性质不是那么明显,不能套用模板,但是思路还是通用的(后面再讲)