二分法边界问题

二分法中最痛苦的问题:确定边界条件。

下面从一个最简单的例子说起:
LeetCode 704. 二分查找

class Solution:
    def search(self, nums: List[int], target: int) -> int:
        left, right = 0, len(nums) # 确定左右边界
        while left < right: # 终止条件
            mid = left + (right-left)//2  #得到中间点
            if nums[mid] == target: return mid
            # 收缩可行的区间
            if nums[mid] < target: left = mid+1
            else: right = mid
        return -1

上面是二分法的标准流程:

确定左右边界,[left, right)
确定终止条件
获得中点
根据比较的值,收缩可行的区间
1、确定左右边界
一个nnn长有序序列的最小索引是000,最大索引是n−1n-1n−1。
那么边界有两种选择:
1、左闭右开 [0,n)[0, n)[0,n)
2、左闭右闭[0,n−1][0, n-1][0,n−1]
两种方式都可以,为了与python的默认用法相一致,我通常使用左闭右开,原因参考为什么区间个切片要忽略最后一个元素。

2、 确定终止条件
首先要明确的是,我们的目标是遍历整个数组。因此终止条件要确保数组的所有元素都可以被包含。
假如我们使用 [0,n)[0, n)[0,n)。此时右值是不可取的,因此终止条件是

while left < right:

如果使用[0,n−1][0, n-1][0,n−1], 此时右值n−1n-1n−1是可取的, 终止条件必须包含。

while left <= right:

上面的说法可能不容易想象,可以想一个简单例子,假设数组只有一个元素,也就是n=1n=1n=1。
左闭右开 [0,1)[0, 1)[0,1) ,不取右值
左闭右闭 [0,0][0, 0][0,0], 取右值
为了将所有元素都包括到终止条件当中,左闭右闭的时候left==right 时必须能够进入while
所以终止条件可以概括为:取右值用<=, 不取右值用<。

3、获得中点
中点不管哪种区间取法都是一样的

mid = left + (right - left) // 2 #写法1
mid = (left + right) // 2 #写法2

写法1是通常采用的,因为这避免直接相加导致的数值溢出
需要明确一点,上述表达式计算出来的 midmidmid必然是合法的索引, 而且不可能等与right,除非 left == right

4、收缩区间
为了避免写出死循环,必须保证每次进入while之后,可行区间必然会缩小。也就是每进入一次while,要么left变大,要么right变小, 如果有不收缩的情况就会死循环。
还假设使用左闭右开[0,n)[0, n)[0,n)
当nums[mid] < target 的时候,应该将left往右移,
先来看下能想到的几种情况,
1、left = mid
2、left = mid + 1
首先明确mid对应值不可能是解,所以mid值要被抛弃。
如果用第方式1,因为left是可以取到的,如果mid 本来就等于left,那么区间没有收缩。所以为了抛弃mid, 必须要mid+1

当nums[mid] > target 时, right要左移。
也看下几种情况
1、right = mid
2、right = mid - 1
还是要明确mid这个值要被抛掉。
如果用方式1、我们知道right这个索引是取不到的,除非leftright, 如果leftright,那么根本进不来while, 所以mid这个索引被扔掉了。方式1可行
方式2, mid这个索引取不到,但是mid-1这个索引也取不到。还不知道mid-1是否是解就被扔掉了,所以方式2不可行。
再看下左闭右闭的情况。原则还是每次都要收缩区间。
两者的差异在右值上面,因为右闭情况下mid == right是有可能的,只要left==right, 而这个条件在while left<=right 情况下是可以满足的。
1、right = mid
2、right = mid - 1
方式1、right是可取到,所以mid没被扔掉
方式2、mid被扔掉了,mid-1是可取的。

总结一下: 每次进入while都要收缩区间, 收缩的办法就是扔掉mid值,那么要显式的抛掉mid,比如left = mid+1, right=mid-1(左闭右闭),要么是隐式收缩,right=mid(左闭右开)

while 退出后 left,right 指向哪儿
在while left < right 的终止条件下,退出时 left == right。
先明确一个问题,left和target的关系是未知的, right和target的关系是已知的。
简单解释下:left = mid + 1。我们知道的是nums[mid]<target,但是nums[mid + 1] 与target的关系是未知的。
但是right = mid,我们知道nums[mid] > target, 所以nums[right]是已知的。除了一种情况,right == n,这种情况下不存在大于target的数字。
因为退出while时候,left == right, 而 nums[right] > target,所以nums[left]>target。实际上left会指向nums中第一个大于target的数字。
已知nums[left] > target,在来看下nums[left-1]和target的关系。
假设left-1存在:

如果执行过赋值left = mid + 1, 那么nums[mid] < target。所以nums[left-1] < target。nums[left-1] < target < nums[left],显然nums[left]是nums中大于target的最小值。
如果left = mid + 1没有执行过, 那么说明left == 0,没有移动过,这就相当于left-1不存在。
如果left-1不存在, left-1不存在说明left == 0, 因为0是第一个索引,所以nums[left]还是第一个大于target的数。

在while left <= right的终止条件下,退出时left == right + 1
这个时候left与target的关系不确定,right和target的关系也不确定。
但是如果执行过赋值 right = mid - 1, nums[mid] > target, 换句话说nums[right+1] == nums[mid] > target。left = right + 1所以nums[left] > target。
如果没执行过right = mid-1, 那么right == n-1, left == right+1 == n。也就是说不存在大于target的数字。
如果执行过赋值left = mid + 1, nums[mid] <target, 那么nums[left-1]==nums[mid] < target < nums[left]。所以left指向第一个大于target的数。
如果没执行过left = mid + 1, 那么left == 0,是第一个索引,所以left仍然是第一个大于target的数字。

综上所以不管哪种情况,left总是指向第一个大于target的数字,或者不存在大于target的数字,此时left == n。

下面是一个示例。

LeetCode 35. 搜索插入位置

class Solution:
    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: return mid
            if nums[mid] < target: left = mid+1
            else: right = mid
        return left # left指向第一个大于target的数字

版权声明:本文为CSDN博主「呆坐的熊」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/Lin_RD/article/details/105186081

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值