二分查找法 ( Binary Search ) 常用于在 有序数组 中 按值查找 某个元素,返回其索引。二分查找可以极大提高搜索效率,其时间复杂度是 O(log N) (N 为数组长度)。我最近在 Leetcode 做了一些二分查找的题目,花了一些时间才弄明白解决问题的思路,在这里归纳总结一下,希望也能帮你更快地梳理思路,欢迎交流讨论呀!
Leetcode 704. 二分查找 是一道标准的二分查找题,要在一个升序整型数组 nums (无重复元素)中寻找目标值 target。如果 target 存在,就返回索引,否则返回 -1。
二分查找的要点是每次把搜索范围缩小一半。具体做法就是每次把当前区间 [ left, right ] 中间点位置的值 nums[middle] 与 target 对比,有三种情况:
如果搜索结束,没有找到符合条件的元素,返回 -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 值最接近的元素的索引。
情况 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 的元素。
思路来源:《算法》(Robert Sedgewick, Kevin Wayne 著)第 3.1节
这个方法应用标准二分查找法,只需改动 while 循环之后的语句 return -1
:
return left
;return right
。为什么呢?接下来我们来分析一下。当数组中没有值为 target 的元素时,因为 while 循环的条件是 left <= right,最后一次循环时搜寻区间有一个或两个元素,right = left 或 left +1,这两种情况时都有 middle = left。
情况一、返回 > target、最接近 target 的元素索引,例如:Leetcode 35. 搜索插入位置 。
因此,只需把标准二分查找代码中 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
即可。
思路来源:01.二分查找知识 | 算法通关手册
方法一,也就是标准二分查找法,是通过不断缩小搜索范围来查找某个元素。但是我们解决这一类型问题时发现,target 的取值可能是介于两个元素中间,虽然 nums[middle] 不等于 target,也许它就是最接近 target 取值的元素,比如对于搜索插入位置的情况,当输入为 nums = [ 1, 3, 5, 7 ] , target = 2, 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 + 1
或 middle = 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,互相学习,多多交流呀!
参考:
《算法》(Robert Sedgewick, Kevin Wayne 著)
https://algo.itcharge.cn/01.Array/03.Array-Binary-Search/01.Array-Binary-Search/