04.数组二分查找

二分查找算法思想

二分查找算法是经典的 「减而治之」 的思想。

这里的 「减」 是减少问题规模的意思,「治」 是解决问题的意思。「减」 和 「治」 结合起来的意思就是 「排除法解决问题」。即:每一次查找,排除掉一定不存在目标元素的区间,在剩下可能存在目标元素的区间中继续查找。

每一次通过一些条件判断,将待搜索的区间逐渐缩小,以达到「减少问题规模」的目的。而于问题的规模是有限的,经过有限次的查找,最终会查找到目标元素或者查找失败


二分查找细节

区间的开闭问题

左闭右闭区间、左闭右开区间指的是初始待查找区间的范围。

  • 左闭右闭区间:初始化时,𝑙𝑒𝑓𝑡=0,𝑟𝑖𝑔ℎ𝑡=𝑙𝑒𝑛(𝑛𝑢𝑚𝑠)−1。

    • 𝑙𝑒𝑓𝑡 为数组第一个元素位置,𝑟𝑖𝑔ℎ𝑡 为数组最后一个元素位置。
    • 区间 [𝑙𝑒𝑓𝑡,𝑟𝑖𝑔ℎ𝑡] 左右边界上的点都能取到。
  • 左闭右开区间:初始化时,𝑙𝑒𝑓𝑡=0,𝑟𝑖𝑔ℎ𝑡=𝑙𝑒𝑛(𝑛𝑢𝑚𝑠)。

    • 𝑙𝑒𝑓𝑡 为数组第一个元素位置,𝑟𝑖𝑔ℎ𝑡 为数组最后一个元素的下一个位置。
    • 区间 [𝑙𝑒𝑓𝑡,𝑟𝑖𝑔ℎ𝑡) 左边界点能取到,而右边界上的点不能取到。

关于二分查找算法的左闭右闭区间、左闭右开区间,其实在网上都有对应的代码。但是相对来说,左闭右开区间这种写法在解决问题的过程中,会使得问题变得复杂,需要考虑的情况更多,所以不建议使用左闭右开区间这种写法,而是建议:全部使用「左闭右闭区间」这种写法


mid的取值问题

在二分查找的实际问题中,最常见的 𝑚𝑖𝑑 取值公式有两个:

  1. mid = (left + right) // 2
  2. mid = (left + right + 1) // 2

式子中 // 所代表的含义是「中间数向下取整」。当待查找区间中的元素个数为奇数个,使用这两种取值公式都能取到中间元素的下标位置。

而当待查找区间中的元素个数为偶数时,使用 mid = (left + right) // 2 式子我们能取到中间靠左边元素的下标位置,使用 mid = (left + right + 1) // 2 式子我们能取到中间靠右边元素的下标位置。

二分查找算法的思路是:根据每次选择中间位置上的数值来决定下一次在哪个区间查找元素。每一次选择的元素位置可以是中间位置,但并不是一定非得是区间中间位置元素,靠左一些、靠右一些、甚至区间三分之一、五分之一处等等,都是可以的。比如说 mid = (left + right) * 1 // 5 也是可以的。

但一般来说,取区间中间位置在平均意义下所达到的效果最好。同时这样写最简单。而对于这两个取值公式,大多数时候是选择第一个公式。不过,有些情况下,是需要考虑第二个公式的,我们会在「4.2 排除法」中进行讲解。

除了上面提到的这两种写法,我们还经常能看到下面两个公式:

  1. mid = left + (right - left) // 2
  2. mid = left + (right - left + 1) // 2

这两个公式其实分别等同于之前两个公式,可以看做是之前两个公式的另一种写法。这种写法能够防止整型溢出问题(Python 语言中整型不会溢出,其他语言可能会有整型溢出问题)。

在 𝑙𝑒𝑓𝑡+𝑟𝑖𝑔ℎ𝑡 的数据量不会超过整型变量最大值时,这两种写法都没有问题。在 𝑙𝑒𝑓𝑡+𝑟𝑖𝑔ℎ𝑡 的数据量可能会超过整型变量最大值时,最好使用第二种写法。所以,为了统一和简化二分查找算法的写法,建议统一写成第二种写法:

  1. mid = left + (right - left) // 2
  2. mid = left + (right - left + 1) // 2

出界条件的判断

二分查找算法的写法中,while 语句出界判断条件通常有两种:

  1. left <= right
  2. left < right

如果判断语句为 left <= right,并且查找的元素不在有序数组中,则 while 语句的出界条件是 left > right,也就是 left == right + 1,写成区间形式就是 [𝑟𝑖𝑔ℎ𝑡+1,𝑟𝑖𝑔ℎ𝑡],此时待查找区间为空,待查找区间中没有元素存在,此时终止循环时,可以直接返回 −1。

  • 比如说区间 [3,2], 此时左边界大于右边界,直接终止循环,返回 −1 即可

 如果判断语句为left < right,并且查找的元素不在有序数组中,则 while 语句出界条件是 left == right,写成区间形式就是 [𝑟𝑖𝑔ℎ𝑡,𝑟𝑖𝑔ℎ𝑡]。此时区间不为空,待查找区间还有一个元素存在,我们并不能确定查找的元素不在这个区间中,此时终止循环时,如果直接返回 −1 就是错误的。 

  • 比如说区间 [2,2],如果元素 𝑛𝑢𝑚𝑠[2] 刚好就是目标元素 𝑡𝑎𝑟𝑔𝑒𝑡,此时终止循环,返回 −1 就漏掉了这个元素。

注意:while判断语句用 left < right 有一个好处,就是在跳出循环的时候,一定是 left == right,我们就不用判断此时应该返回 𝑙𝑒𝑓𝑡 还是 𝑟𝑖𝑔ℎ𝑡 了。

搜索范围的选择

在进行区间范围选择的时候,通常有三种写法:

  1. left = mid + 1right = mid - 1
  2. left = mid + 1 right = mid
  3. left = midright = mid - 1。 
  • 思路 1:「直接法」—— 在循环体中找到元素后直接返回结果。
  • 思路 2:「排除法」—— 在循环体中排除目标元素一定不存在区间。

练习

 T0704. 二分查找

class Solution:
    def search(self, nums: List[int], target: int) -> int:
        left, right = 0, len(nums) - 1
        
        # 在区间 [left, right] 内查找 target
        while left <= right:
            # 取区间中间节点
            mid = (left + right) // 2
            # 如果找到目标值,则直接返回中心位置
            if nums[mid] == target:
                return mid
            # 如果 nums[mid] 小于目标值,则在 [mid + 1, right] 中继续搜索
            elif nums[mid] < target:
                left = mid + 1
            # 如果 nums[mid] 大于目标值,则在 [left, mid - 1] 中继续搜索
            else:
                right = mid - 1
        # 未搜索到元素,返回 -1
        return -1

 T0035. 搜索插入位置

class Solution:
    def searchInsert(self, nums: List[int], target: int) -> int:
        size = len(nums)
        left, right = 0, size - 1

        while left <= right:
            mid = left + (right - left) // 2
            if nums[mid] == target:
                return mid
            elif nums[mid] < target:
                left = mid + 1
            else:
                right = mid - 1

        return left

T0374. 猜数字大小

class Solution:
    def guessNumber(self, n: int) -> int:
        left = 1
        right = n
        while left <= right:
            mid = left + (right - left) // 2
            ans = guess(mid)
            if ans == 1:
                left = mid + 1
            elif ans == -1:
                right = mid - 1
            else:
                return mid
        return 0

T0069. x 的平方根

class Solution:
    def mySqrt(self, x: int) -> int:
        left = 0
        right = x
        ans = -1
        while left <= right:
            mid = (left + right) // 2
            if mid * mid <= x:
                ans = mid
                left = mid + 1
            else:
                right = mid - 1
        return ans

  • 45
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值