二分搜索,你还在用三个模版?一个就够了!

One Template For All Binary Search Problems

Posted by Eric.Y on May 20, 2022

二分搜索,你还在用三个模版?一个就够了!

One Template for All Binary Search Problems

背景

首先,面对二分搜索,我们往往会碰到以下疑问:

  • leftright 初始值?

  • 循环条件是 left < right 还是left <= right

  • 如何更新leftrightleft = midleft = mid + 1right = 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

几个注意点:

  • leftright 是解空间的闭区间
  • 循环结束时,**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

几个注意点:

  1. right = len(nums):上面提到,leftright 是解空间的闭区间;当 target 比所有值大时,必定是落在索引len(nums)上面。
  2. 返回值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]

几个注意点:

  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]

几个注意点:

  1. right = len(nums):上面提到,leftright 是解空间的闭区间;当 target 比所有值大时,必定是落在索引len(nums)上面。
  2. 返回值left是满足nums[mid] > target的最小值,因此解是left-1
  3. 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]的最小值


综合应用题

力扣

  1. 寻找分割点:left 是满足条件的最小值,因此我们找的是右半段的左边界,并且由此计算左半段的右边界,重新赋值给left
  2. 得到两段为 [start, left][left + 1, end]
  3. 判断 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

结论

记住下面两句话,重新刷一下二分搜索,你会发现如鱼得水。

  • leftright 是解空间的闭区间
  • 循环结束时,**left** 是满足 **condition** 的最小值