二分查找
问题定义:
给定一个由数字组成的有序数组nums ,并给你一个数字 target 。问 nums 中是否存在target 。如果存在, 则返回其在 nums 中的索引。如果不存在,则返回 1 。
常见变体有:
如果存在多个满足条件的元素,返回最左边满足条件的索引。
如果存在多个满足条件的元素,返回最右边满足条件的索引。
数组不是整体有序的,比如先升序再降序,或者先降序再升序。
将一维数组变成二维数组。
二分查找的前提
数组必须时有序的(如果数组时无序的,我们可以考虑对数组进行排序,但是要考虑排序的复杂度)
常见题型
查找一个数
算法描述:
-
先从数组的中间元素开始,如果中间元素正好是要查找的元素,则搜索过程结束;
-
如果目标元素大于中间元素,则在数组大于中间元素的那一半中查找,而且跟开始一样从中间元素开始比较。
-
如果目标元素小于中间元素,则在数组小于中间元素的那一半中查找,而且跟开始一样从中间元素开始比较。
-
如果在某一步骤数组为空,则代表找不到。
一般的二分查找
思维框架
首先定义搜索区间为 [left, right],注意是左右都闭合,之后会用到这个点
由于定义的搜索区间为[left, right],因此当 left <= right 的时候,搜索区间都不为空,此时我们都需要继续搜索。 也就是说终止搜索条件应该为 left <= right 。
循环体内,我们不断计算mid ,并将 nums [mid] 与 目标值比对。
如果nums [mid] 等于目标值, 则提前返回 mid (只需要找到一个满足条件的即可)
如果nums [mid] 小于目标值, 说明目标值在 mid 右侧,这个时候搜索区间可缩小为[mid + 1, right](mid 以及 mid 左侧的数字被我们排除在外)
如果nums [mid] 大于目标值, 说明目标值在 mid 左侧,这个时候搜索区间可缩小为[left, mid-1] (mid 以及 mid 右侧的数字被我们排除在外)
循环结束都没有找到,则说明找不到,返回-1 表示未找到。
代码模板
def binary_search(nums, target):
left = 0
right = len(nums) - 1
while left <= right:
mid = (left + right) >> 1
if nums[mid] == target:
return mid
elif nums[mid] < target:
left = mid + 1
elif nums[mid] > target:
right = mid - 1
return -1
寻找最左边的满足条件的值
思维框架
首先定义搜索区间为[left, right],注意是左右都闭合,之后会用到这个点。终止搜索条件为left <= right 。
循环体内,我们不断计算mid ,并将 nums [mid] 与 目标值比对。
如果nums [mid] 等于目标值, 则收缩右边界,我们找到了一个备胎,继续看看左边还有没有了(注意这里不一样)
如果nums [mid] 小于目标值, 说明目标值在 mid 右侧,这个时候搜索区间可缩小为 [mid + 1,right]
如果nums [mid] 大于目标值, 说明目标值在 mid 左侧,这个时候搜索区间可缩小为 [left, mid-1]
由于不会提前返回,因此我们需要检查最终的left ,看 nums[left]是否等于 target 。
如果不等于target ,或者 left 出了右边边界了,说明至死都没有找到一个备胎,则返回 -1.否则返回
left 即可,备胎转正。
代码模板
def binary_search_left(nums,target):
left = 0
right = len(nums)-1
while left<=right:
mid = (left+right)>>1
# 使得right在target的左边一个
if nums[mid] == target:
right = mid-1
elif nums[mid] >target:
right = mid -1
elif nums[mid]<target:
left = mid+1
# 循环结束时,left>right,如果left没有超出数组,此时right又在target左边一个,
# 即left=right+1,之前找到target时的坐标就是right+1
if left>= len(nums) or nums[left] !=target :
return -1
return left
寻找最右边的满足条件的值
思维框架
首先定义搜索区间为[left, right],注意是左右都闭合,之后会用到这个点。
由于我们定义的搜索区间为[left, right],因此当 left <= right 的时候,搜索区间都不为空。 也就
是说我们的终止搜索条件为 left <= right 。
循环体内,我们不断计算mid ,并将 nums [mid] 与 目标值比对。
如果nums [mid] 等于目标值, 则收缩左边界,我们找到了一个备胎,继续看看右边还有没有了
如果nums [mid] 小于目标值, 说明目标值在 mid 右侧,这个时候搜索区间可缩小为 [mid + 1,right]
如果nums [mid] 大于目标值, 说明目标值在 mid 左侧,这个时候搜索区间可缩小为 [left, mid-1]
由于不会提前返回,因此我们需要检查最终的right ,看 nums[right]是否等于 target 。
如果不等于target ,或者 right 出了左边边界了,说明至死都没有找到一个备胎,则返回-1.
否则返回right 即可,备胎转正。
代码
def binary_search_right(nums,target):
left = 0
right = len(nums)-1
while left<=right:
mid = (left+right)>>1
if nums[mid] == target:
left = mid+1
elif nums[mid]>target:
right = mid -1
elif nums[mid]<target:
left = mid+1
if right<0 or nums[right] != target:
return -1
return right
寻找最左插入位置
比如一个数组nums : [1,3,4 ],target 是 2 。
我们应该将其插入(注意不是真的插入)的位置是索引 1 的位置,即 [1,2,3,4] 。因此寻找最左插入位置应该返回 1 ,而寻找最左满足条件应该返回 -1 。
思维框架
如果你将寻找最左插入位置看成是寻找最左满足大于等于x 的值,那就可以和前面的知识产生联系,使
得代码更加统一。唯一的区别点在于前面是最左满足等于 x ,这里是最左满足大于等于 x 。
具体算法:
首先定义搜索区间为[left, right],注意是左右都闭合,之后会用到这个点。
由于我们定义的搜索区间为[left, right],因此当 left <= right 的时候,搜索区间都不为空。 也就是
说我们的终止搜索条件为 left <= right 。
当nums [mid] >= x ,说明找到一个备胎,我们令 right = mid-1 将 mid 从搜索区间排除,继续看看
有没有更好的备胎。
当nums [mid] < x ,说明 mid 根本就不是答案,直接更新 left = mid + 1 ,从而将 mid 从搜索区间排
除。
最后搜索区间的left 就是最好的备胎,备胎转正。
def bisect_left(nums,target):
left = 0
right = len(nums)-1
while left<= right:
mid = (left+right)>>1
if nums[mid]>=target:
right = mid-1
elif nums[mid]<target:
left = mid+1
return left
寻找最右插入位置
如果你将寻找最右插入位置看成是寻找最右满足大于x 的值,那就可以和前面的知识产生联系,使得代码更加统一。唯一的区别点在于前面是最右满足等于 x ,这里是最右满足大于x 。
def bisect_right(nums,target):
left = 0
right = len(nums)-1
while left<=right:
mid = (left+right)>>1
if nums[mid] > target:
right = mid-1
if nums[mid]<=target:
left = mid+1
return left
局部有序(先降后升或先升后降)
33搜索旋转排序数组
81搜索旋转排序数组 II
面试题10.03. 搜索旋转数组
34排序数组中查找元素的第一个和最后一个位置
35搜索插入位置
33.搜索旋转排序数组
class Solution:
def search(self, nums: List[int], target: int) -> int:
if not nums:
return -1
n = len(nums)
def find_min():
begin = 0
end = n-1
# 第一个数小于等于最后一个数,则说明数组有序
if nums[begin]<=nums[end]:
return begin
#
while begin<=end:
mid = (begin + end)>>1
#如果某个数大于后面的一个数,则后面的这个数就是最小的数,
#否则有序数组后一个数一定大于前一个数
if mid <n-1 and nums[mid]>nums[mid+1]:
return mid+1
# 前半段有序,则在后半段中继续找
if nums[0] <= nums[mid]:
begin = mid+1
else :
end = mid-1
return 0
def binarty_search(start,end):
while start<= end:
mid = (start+end)>>1
if nums[mid] == target:
return mid
elif nums[mid] > target:
end = mid -1
else :
start = mid+1
return -1
# 找到最小值的索引
min_index = find_min()
# 如果该数组是一个有序数组,则直接进行二分查找
if min_index == 0:
return binarty_search(0,n-1)
# 如果数组的第一个小于target,则只能在前半段的升序查找,即在最小的索引之前
if nums[0]<=target:
return binarty_search(0,min_index-1)
return binarty_search(min_index,n-1)
class Solution:
def search(self, nums: List[int], target: int) -> int:
if not nums:
return -1
left = 0
right