二分查找法搜寻元素 Leetcode35, Leetcode69

二分查找法 ( Binary Search ) 常用于在 有序数组按值查找 某个元素,返回其索引。二分查找可以极大提高搜索效率,其时间复杂度是 O(log N) (N 为数组长度)。我最近在 Leetcode 做了一些二分查找的题目,花了一些时间才弄明白解决问题的思路,在这里归纳总结一下,希望也能帮你更快地梳理思路,欢迎交流讨论呀!

一、标准二分查找 :查找某个元素

Leetcode 704. 二分查找 是一道标准的二分查找题,要在一个升序整型数组 nums (无重复元素)中寻找目标值 target。如果 target 存在,就返回索引,否则返回 -1。

二分查找的要点是每次把搜索范围缩小一半。具体做法就是每次把当前区间 [ left, right ] 中间点位置的值 nums[middle] 与 target 对比,有三种情况:

  • 如果 nums[middle] 等于 target,这就是要找的元素。
  • 如果 nums[middle] 小于 target,接下来只在大于中间点的区间 [ middle + 1, right ] 搜索。
  • 如果 nums[middle] 大于 target,接下来只要在小于中间点的区间 [ left, middle - 1 ] 搜索。

如果搜索结束,没有找到符合条件的元素,返回 -1。

对应的 Python 代码如下:

left, right = 0, len(nums)-1

while left <= right:
    # 这里求 middle的表达式与 (left + right)//2 相同
    # 这样写是为了防止数值太大时,相加运算溢出   
    middle = left + (right - left) // 2
    if nums[middle] == target:
        return middle
    elif nums[middle] < target:
        left = middle + 1
    else:
        right = middle - 1

return -1

这道问题对于没找到值为 target 的元素的情况,只要返回 -1 即可。如果增加一点难度,当没有在数组中找到值为 target 的元素时,要返回一个与它最接近的元素,那应该怎么办呢?接下来我们就来分析解决这一类问题。

二、查找取值与 target 最接近的元素

这类问题增加了 对于数组中没有值为 target 的元素时的处理,此时依然要返回一个索引,就是与 target 值最接近的元素的索引。

情况 1 :查找 >= target 、且与 target 值最接近的元素,例如, Leetcode 35. 搜索插入位置 ,当数组中没有找到值为 target 的元素时, 要求返回把 target 按顺序插入数组时的位置。例如,输入 nums = [ 1, 3, 5, 7 ], target = 2,输出为 1,也就是元素 3 的索引。在输入 nums 中,元素 3 是大于 target 的元素中、最接近 target 的元素。

情况 2 :查找 <= target 、且与 target 值最接近的元素,例如, Leetcode 69. x 的平方根 ,求一个非负整数 x 的平方根,要求返回结果是整型。如果遇到平方根不是整数的情况呢?只取整数部分。例如,输入 x = 8,输出为 2。8 的平方根也就是 target 值,是小数 2.82842…。2 是小于 target 的元素中、最接近 target 的元素。

方法一、标准二分查找法 while left <= right

这个方法应用标准二分查找法,只需改动 while 循环之后的语句 return -1

  • 如果查找的是 >= target 的元素,改为 return left
  • 如果查找的是 <= target 的元素,改为 return right

为什么呢?接下来我们来分析一下。当数组中没有值为 target 的元素时,因为 while 循环的条件是 left <= right,最后一次循环时搜寻区间有一个或两个元素,right = left 或 left +1,这两种情况时都有 middle = left。

情况一、返回 > target、最接近 target 的元素索引,例如:Leetcode 35. 搜索插入位置

  • 如果最后一次 while 循环时 nums[middle] > target,元素应该插入的位置是 middle,而循环结束时 left = middle。
  • 如果最后一次 while 循环时 nums[middle] < target,元素应该插入的位置是 middle + 1,而循环结束时 left = middle +1。

因此,只需把标准二分查找代码中 while 循环之后的语句由 return -1 改为 return left 即可。

情况二、返回 < target、最接近 target 的元素索引,例如:Leetcode 69. x 的平方根

  • 如果最后一次 while 循环时 nums[middle] > target,元素应该插入的位置是 middle - 1 ,而循环结束时 right = middle -1。

  • 如果最后一次 while 循环时 nums[middle] < target,元素应该插入的位置是 middle。循环开始时只有 right = left = middle(如果 while 循环开始时 right = left + 1,而 nums[middle] < target,还会进入下一次循环,因此排除这种情况。),结束时 right = middle。

因此, 只需把标准二分查找代码中 while 循环之后的语句由 return -1 改为 return right 即可。

方法二、另一种二分查找法 while left < right

方法一,也就是标准二分查找法,是通过不断缩小搜索范围来查找某个元素。但是我们解决这一类型问题时发现,target 的取值可能是介于两个元素中间,虽然 nums[middle] 不等于 target,也许它就是最接近 target 取值的元素,比如对于搜索插入位置的情况,当输入为 nums = [ 1, 3, 5, 7 ] , target = 2, middle = 1, nums[middle] = 3 时。因此,我们保留 middle 位置元素进入下一次搜寻,这就是方法二的思路。

情况一、返回 > target、最接近 target 的元素索引,例如:Leetcode 35. 搜索插入位置

这种情况下,当 nums[middle] > target 时,middle 位置有可能是我们要找的插入位置,下一次搜寻区间应该包含 middle。因此,此时 right = middle

与方法一不同,这里的 while 循环条件为 left < right,因此循环终止时有 left = right,这样搜寻区间还有一个元素。这就是 >= target、最接近 target 的元素,最终返回它的索引( left 或 right 都可以,两者相同)。

Python 代码如下:

# 如果target不大于nums的第一个元素,直接返回索引0
if target <= nums[0]:
    return 0
# 如果target大于nums的最后一个元素,直接返回数组长度
if target > nums[len(nums)-1]:
    return len(nums)

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

情况二、返回 < target、最接近 target 的元素索引,例如:Leetcode 69. x 的平方根

这种情况下,当 nums[middle] < target 时,middle 位置有可能是我们要找的,下一次搜寻区间应该包含 middle。因此,此时 left = middle

这里有一个小细节需要注意,因为 while 循环条件为 left < right,最后一次循环时搜寻区间有两个元素,当 left = right 时循环结束。但是我们求 middle 并不是精确的平均值,而是向下取整,这导致当搜寻区间只有两个元素时,middle 始终等于 left。 这样,当 nums[middle] < target 时,left = middle,下一次循环 middle = (left + right) /2 = left ,没有更新搜寻区间,循环无法停止。怎么办呢?

可以在求均值时对 middle 向上取整,比如 middle = left + (right - left) // 2 + 1middle = left + (right - left + 1) // 2

Python 代码如下:

# x=0或1时,直接返回结果
if x <= 1:
    return x

left, right = 1, x
while left < right:
    middle = left + (right - left) // 2 + 1
    if middle ** 2 == x:
        return middle
    elif middle ** 2 > x:
        right = middle - 1
    else:
        left =  middle
        
return left

本文对您有帮助的话,请点赞支持一下吧,谢谢!

关注我 宁萌Julie,互相学习,多多交流呀!

参考

  1. 标准二分查找法:《算法》(Robert Sedgewick, Kevin Wayne 著)第三章 3.1节

  2. 这篇文章对两种二分查找法的要点及区别讲解得清晰全面:01.二分查找知识 | 算法通关手册

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值