查找算法
有序查找:二分查找(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,要保证所有元素都不漏,可以确定
初始条件
的选择和开闭区间
的选择初始条件1: 如果第一次就取右开,那永远验证不到右边界
初始条件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
会在查找到右边界时出现问题。此时,我们发现问题并不是出现在循环的第一步,而是出现在了循环的最后一步。这是因为分治问题的特性,使得最后一步处理问题的性质,一定与第一步处理问题的性质相同。所以当改了第一步的区间开闭时,并没有影响第一步的结果,但影响了最后一步的结果
-
由区间开闭,可以确定循环终止条件。既然我们已经确定了,二分法其实就是将一个大问题,拆解为一个小问题求解,那么此时,循环终止一定是在问题拆解到最小可求解的时候,才会循环终止。
对于
初始条件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)
解题关键
- 同一元素不要在循环中多次处理
- 区分好区间开闭
- 根据区间开闭选择循环终止条件
算法特性
- 适用条件:有序数组
- 时间复杂度:O(log n)(小于O(n))