二分搜索,你还在用三个模版?一个就够了!
One Template for All Binary Search Problems
背景
首先,面对二分搜索,我们往往会碰到以下疑问:
-
left
和right
初始值? -
循环条件是
left < right
还是left <= right
? -
如何更新
left
和right
?left = mid
,left = mid + 1
,right = mid
,right = mid — 1
? -
结束时要选
left
还是right
?
另外,二分搜索有多个场景,
-
标准的二分查找
-
二分查找左边界
-
二分查找右边界
-
二分查找极值点
对于上述多个场景,网上也有相关的模板,但是很多都是使用多个模版来针对性解决不同场景的应用问题。
但是,其实只要一个模板就够了!可能有很多大佬在用,本文只作总结,并提炼出统一的方法来应对上述不同场景。
模版
def binary_search(array) -> int:
def condition(value) -> bool:
pass
left, right = search_space_
while left < right:
mid = left + (right - left) // 2
if condition(mid):
right = mid
else:
left = mid + 1
return left
几个注意点:
left
和right
是解空间的闭区间- 循环结束时,
**left**
是满足**condition**
的最小值 right
的取值是有讲究的,下面会提到
应用
下面使用这一个模板来解决几个常见场景的问题。
寻找插入点
https://leetcode.cn/problems/search-insert-position
def searchInsert(self, nums: List[int], target: int) -> int:
left, right = 0, len(nums)
while left < right:
mid = left + (right - left) // 2
if nums[mid] >= target:
right = mid
else:
left = mid + 1
return left
几个注意点:
right = len(nums)
:上面提到,left
和right
是解空间的闭区间;当 target 比所有值大时,必定是落在索引len(nums)
上面。- 返回值
left
是满足nums[mid] ≥ target
的最小值,即要么等于要么大于
寻找左边界
# ====== left bound ===========
left, right = 0, len(nums) - 1
while left < right:
mid = left + (right - left) // 2
if nums[mid] >= target:
right = mid
else:
left = mid + 1
if nums[left] != target:
return [-1, -1]
几个注意点:
- 返回值
left
是满足nums[mid] ≥ target
的最小值:即要么找到等于 target 的最小值,返回该左边界;要么找到大于 target 的最小值,返回 -1
寻找右边界
# ====== right bound ===========
left, right = 0, len(nums)
while left < right:
mid = left + (right - left) // 2
if nums[mid] > target:
right = mid
else:
left = mid + 1
if left == 0:
if nums[left] != target:
return -1
else:
return 0
if nums[left - 1] != target:
return [-1, -1]
几个注意点:
right = len(nums)
:上面提到,left
和right
是解空间的闭区间;当 target 比所有值大时,必定是落在索引len(nums)
上面。- 返回值
left
是满足nums[mid] > target
的最小值,因此解是left-1
if left == 0
的判断:因为返回值是left-1
,因此多了这么一步判断
寻找极值点
def findPeakElement(self, nums: List[int]) -> int:
left, right = 0, len(nums) - 1
while left < right:
mid = left + (right - left) // 2
if nums[mid] > nums[mid + 1]:
right = mid
else:
left = mid + 1
return left
注意点:返回值left
是满足nums[mid] > nums[mid+1]
的最小值
综合应用题
- 寻找分割点:
left
是满足条件的最小值,因此我们找的是右半段的左边界,并且由此计算左半段的右边界,重新赋值给left
; - 得到两段为
[start, left]
和[left + 1, end]
; - 判断
target
所在的段,并进行普通二分搜索
def search(self, nums: List[int], target: int) -> bool:
n = len(nums)
start, end = 0, n - 1
while start < end and nums[start] == nums[end]:
end -= 1
# 1. 寻找分割点(小于 nums[start] 的第一个值)
left, right = start, end
while left < right:
mid = left + (right - left) // 2
if nums[mid] < nums[start]:
right = mid
else:
left = mid + 1
# 1.1. 如果存在分割点,则将 left 作为左半段的右边界
if nums[start] > nums[left]:
left = left - 1
if nums[start] <= target:
left, right = start, left
else:
left, right = left + 1, end
# 2. 普通二分搜索
while left < right:
mid = left + (right - left) // 2
if nums[mid] >= target:
right = mid
else:
left = mid + 1
# 2.2. 小插曲,通过测试用例排除
# 出现的原因是上面进行了 left, right = left + 1, end 的赋值
# 导致 left > right,因此需要作此判断
if left > end:
return False
return True if nums[left] == target else False
结论
记住下面两句话,重新刷一下二分搜索,你会发现如鱼得水。
left
和right
是解空间的闭区间- 循环结束时,
**left**
是满足**condition**
的最小值