二分查找(从分治思想谈二分查找原理,一次搞定边界条件)

查找算法

有序查找:二分查找(704)

例题(704)

给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。

示例 1:

输入: nums = [-1,0,3,5,9,12], target = 9
输出: 4
解释: 9 出现在 nums 中并且下标为 4

代码:

class Solution:
	# 初始条件一[]
    def binSearch111(self, nums: List[int], target: int) -> int:
        left = 0
        right = len(nums)-1

        # []问题,所以<=
        while left <= right:
            # 不使用((n+1) + (n-1)) // 2公式,避免溢出
            mid = left + (right - left) // 2
            # 拆分为[left,mid-1],mid,[mid+1,right]两个问题
            if nums[mid] == target:
                return mid
            elif nums[mid] > target:
                right = mid -1
            elif nums[mid] < target:
                left = mid + 1
        return -1
        
	# 初始条件二[)
    def binSearch(self, nums: List[int], target: int) -> int:
        left = 0
        right = len(nums)

        # [)问题,所以<
        while left < right:
            mid = left + (right - left) // 2
            # [left,mid),mid,[mid+1,right)两个问题
            # 如果 right = mid -1,下一轮循环变为[left,mid-1),mid,[mid+1,right),此时mid-1得不到验证
            # 如果 left = mid,下一轮循环变为[left,mid),mid,[mid,right),此时mid验了两遍
            if nums[mid] == target:
                return mid
            elif nums[mid] > target:
                right = mid
            elif nums[mid] < target:
                left = mid + 1
        return -1
难点分析:循环条件的分析判断

分治理论

二分法是分治思想的典范,这句话很多人听过只觉得是个概念,不当回事,但实际上这才是解决二分法问题的核心关键。

什么是分治思想,分治思想就是把一个大问题A,拆成无数个小问题B,再采用相同的方法分而治之。典型的案例是动态规划问题的应用,二分法也可以理解为一个简单的动态规划问题,只不过由于二分法逐渐缩小范围,实际上是将其他不符合要求的小问题抛弃了。

既然是分治,那就可以把每次循环,看成一个问题的拆分动作。每次缩小范围后,都视为在处理一个拆分后的小问题。

由于解决大问题和小问题采用的是同一套方法,那么拆分后的小问题,与大问题一定要有相同的性质。

所以拆分时,要遵照以下核心思想:

  1. 拆分等价性:拆分过程中不能漏元素,同一个元素也不能再多次循环终处理多遍
  2. 大小问题性质相似:由于处理方法相同,所以大小问题区间开合相同

思考流程

  1. 根据核心思想1,要保证所有元素都不漏,可以确定初始条件的选择和开闭区间的选择

    初始条件1: 如果第一次就取右开,那永远验证不到右边界

    初始条件2: 右侧超出数组范围,不能取右闭

  2. 根据核心思想2,要保证在多次循环中,要求区间开合相同。同时,按照核心思想1,还要满足同一个元素不能再多次循环终处理多遍

    假设A-H分别为数组下标(即A=0,B=1等等,为了区分元素和下标也是拼了),I为数组长度

    ①首先最简单的拆分

    比如[A,H]类问题 [A,H] = [A,C]+[D,H]

    比如[A,I)类问题 [A,I) = [A,C)+[C,I)

    ②此时一个大问题就拆成了两个小问题,再去掉已经验证过的元素,假设为D

    [A,H] = [A,C] + [D] + [E,H]

    [A,H] = [A,C) + [C,D)+ D + [E,I)

    ③此时就可以将已验证过舍弃 的部分去掉,剩下的部分继续拆解

    比如假设nums = [1,2,3,4,5,6,8,9],target = 7 ,利用此种拆分的方式拆分结果如下

    初始条件1 :[A,H] = [A,C](舍弃) + [D](已验证) + [E,H](继续拆分)= [A,C] + [D]+ [E](舍弃) + [F](已验证) + [G,H](继续拆分)

    初始条件2 :[A,I) = [A,D)(舍弃) + D(已验证) + [E,I)(继续拆分)= [A,D) + [D]+ [E,F)(舍弃) + [F](已验证) + [G,I](继续拆分)

    问题一:为什么要保证区间开合相同?可不可以让 [A,H) = [A,C](右闭) + [C,H)呢?

    答:不可以,因为循环内容一定的情况下,是没有办法保证在处理[A,C] 时处理了C,而在处理[C,H)时,不处理H的

    问题二:在使用二分法时,第一步求得是(left + right)/2, 区间的开闭对第一步好像并没有什么影响,如果我不用[A,I) ,而是用了[A,I](右闭),会出现什么问题呢?

    答:当target元素不在数组内,且target>nums[H],此时会校验nums[I],但5超出了数组下标。比如nums = [1,2,5,7,9],target = 10 会在查找到右边界时出现问题。

    此时,我们发现问题并不是出现在循环的第一步,而是出现在了循环的最后一步。这是因为分治问题的特性,使得最后一步处理问题的性质,一定与第一步处理问题的性质相同。所以当改了第一步的区间开闭时,并没有影响第一步的结果,但影响了最后一步的结果

  3. 由区间开闭,可以确定循环终止条件。既然我们已经确定了,二分法其实就是将一个大问题,拆解为一个小问题求解,那么此时,循环终止一定是在问题拆解到最小可求解的时候,才会循环终止。

    对于初始条件1 :[A,H] 的情况,当最小问题时候,理论上应该考虑是[F,G],但是此时发现(F+G)/2===F,right 还会左移一步,所以最小问题并不是[F,G] 而是[F], 此时[F]还没有得到验证,所以还需要最后运行一次,即left==right 运行完成后终止,循环条件为while(left <= right)

    对于初始条件2 :[A,I) 的情况,当最小问题时候是[F,G),毋庸置疑。此时只需要再验证一次F即可,即left < right 运行完成后终止,,循环条件为while(left < right)

解题关键
  1. 同一元素不要在循环中多次处理
  2. 区分好区间开闭
  3. 根据区间开闭选择循环终止条件
算法特性
  1. 适用条件:有序数组
  2. 时间复杂度:O(log n)(小于O(n))
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值