给定排序数组nums,搜索某个数的索引。
需要定义搜索区间的形式,是左闭右开、两边闭。区间的开闭还决定了区间的初始条件、循环的终止条件是否需要带等号以及新的搜索区间的生成。
例:nums = [0,2,3,5,7,9,12,13,14],target=14
两边闭的区间:
init:
初始搜索区间为:[0, 8]
loop1:
搜索范围为:[0, 8]
中间索引为:mid = 4, nums[4] = 7
判断下一次搜索范围: 7 < 14, 往右边搜索 l = 4 + 1 = 5
loop2:
搜索范围为:[5, 8]
中间索引为:mid = 6, nums[6] = 12
判断下一次搜索范围:12 < 14, 往右边搜索 l = 6 + 1 = 7
loop3:
搜索范围为: [7, 8]
中间索引为:mid = 7, nums[7] = 13
判断下一次搜索范围: 13 < 14,往右边搜索 l = 7 + 1 = 8
lopp4:
搜索范围为:[8, 8]
中间索引为:mid = 8, nums[8] = 14
当前mid所在的位置数值等于搜索的值,直接返回mid
-
初始搜索区间:初始的搜索区间为[0, n-1]
-
生成新的搜索区间:两边闭区间每次循环判断nums[mid] == target之后,若不相等,新的搜索区间会将mid的左右两个数作为新的端点,这是因为mid已经搜索过了,所以不需要把mid包含在新的搜索空间中;而且如果不加减1,会出现bug,考虑搜索范围:[a, a+1],mid=a,如果搜索目标在a+1的位置,下一次搜索范围仍是l = a, r = a+1的话就会陷入死循环,因此需要使用mid右边的一个点作为新的端点,[a+1, a+1],最后得到结果,并且没有死循环。
-
循环终止条件:循环的终止条件是当左端点比右端点大时,如:[a+1, a]就不是一个合法的两边闭的区间,就终止循环,while的条件写作:l <= r,等号不能去,去掉之后对于只有一个元素的区间[a, a]还没搜索就退出,返回了-1
(计算mid时,采用l + (r - l) //2而不使用(l + r) // 2是防止整数溢出,当然python不用害怕这点)
def search(nums, target):
# 初始搜索区间
l, r = 0, len(nums)-1
# 终止条件
while l <= r:
mid = l + (r - l) // 2
if target == nums[mid]:
return mid
elif nums[mid] > target:
# 生成新的搜索区间
r = mid-1
else:
l = mid+1
return -1
左闭右开区间:
例:nums = [0,2,3,5,7,9,12,13,14],target=5
init:
初始化搜索区间为:[0, 9)
loop1:
搜索区间为:[0, 9)
中间索引为:mid = 4, nums[mid] = 7
判断下一次搜索区间, 5 < 7,往左边搜索:r = 4
loop2:
搜索区间为:[0, 4)
中间索引为:mid = 2, nums[mid] = 3
判断下一次搜索区间, 5 > 3,往右边搜索:l = 3
lopp3:
搜索区间为:[3, 4)
中间索引为:mid = 3, nums[mid] = 5
当前mid所在的位置数值等于搜索的值,直接返回mid
左闭右开区间的判断条件与两边闭的区间有三处不同:
-
初始条件:[0, 9),即右端点为n而不是n-1
-
循环终止条件:l < r即可,因为区间[a, a+1)只包含nums[a]这个数,因此不用和上面一样取等号了,加上等号判断区间时,[a, a)反而不是一个合法区间了
-
生成新的搜索区间:闭合的端点处在上一次循环中已经判断过了,因此需要用上一次循环mid的左右两边的索引做端点;而开区间不需要加减1,因为本身开区间端点处就没有包含这个端点。
def search(nums, target):
l, r = 0, len(nums)
while l < r:
mid = l + (r - l) // 2
if target == nums[mid]:
return mid
elif nums[mid] > target:
r = mid
else:
l = mid+1
return -1
给定升序数组:[1, 2, 2, 2, 3],target=2,求target在数组中的左边界。
上面的原始二分搜索就无法求出想要的结果,它会在第一个循环时就结束,返回中间索引2,因为第一次循环就判断出中间的数满足target=2,但是并不满足题目要求。当target不存在于数组中时,返回的是数组中小于target的元素个数。
修改一下上面的二分查找,当nums[mid]=target时,说明左边界还在区间的左边;否则还是和前面一样做处理
init:
初始化搜索区间为:[0, 4]
loop1:
搜索区间为:[0, 4]
中间索引为:mid = 2, nums[2] = 2
判断下一次搜索区间,2 = 2,说明边界还在数组左侧,往左搜索:r = 2-1 = 1
loop2:
搜索区间为:[0, 1]
中间索引为:mid = 0, nums[0] = 1
判断下一次搜索区间,1 < 2,说明边界在右半部分,往右搜索:l = 0+1 = 1
loop3:
搜索区间为:[1, 1]
中间索引为:mid = 1, nums[1] = 2
此时2 = 2,满足条件,但是还是要继续循环,往左搜索:r = 1-1 = 0
此时l = 1, r = 0,不满足条件,退出循环,并且l所指向的索引就是左边界,但是r不是右边界
算法基本上与上面一致,但是当发现nums[mid]=target时我们并不急着返回结果,而是继续尝试搜索左边界,直到退出循环。
def search_boundary(nums, target):
l, r = 0, len(nums)-1
while l <= r:
mid = l + (r - l) // 2
# 判断条件可以进一步优化,将if和else分支合并到一起
if nums[mid] == target:
r = mid - 1
elif nums[mid] < target:
l = mid + 1
else:
r = mid -1
return l
当使用两边闭合的区间时,只能使用左指针作为返回值,因为其循环终止条件为:l <= r,退出循环的条件是l 和 r不相等,因此只能用l作为左边界。而使用左闭右开的区间时,就不用考虑这一点。
def search_boundary(nums, target):
l, r = 0, len(nums)
while l < r:
mid = l + (r - l) // 2
if nums[mid] < target:
l = mid + 1
else:
r = mid
return l
当target不存在于数组中时要想返回-1,可以在最后判断:
return l if nums[l] == target else -1
寻找右侧边界同理,当发现target=nums[mid]时, 右侧边界肯定还在右边,因此设置l = mid+1
init:
初始搜索区间为:[0, 4]
lopp1:
搜索区间:[0, 4]
中间索引为:mid = 2, nums[2]=2
判断下一次循环搜索区间:2 = 2,往右搜索:l = 2+1 = 3
loop2:
搜索区间:[3, 4]
中间索引为:mid = 3, nums[3]=2
判断下一次循环搜索区间:2 = 2,往右搜索:l = 3+1 = 4
loop3:
搜索区间:[4, 4]
中间索引为:mid = 4, nums[4]=3
判断下一次循环搜索区间:2 < 3,往左搜索:r = mid-1
此时l > r,并且r指向右边界
def search_boundary_right(nums, target):
l, r = 0, len(nums)-1
while l <= r:
mid = l + (r - l) // 2
if nums[mid] == target:
l = mid + 1
elif nums[mid] < target:
l = mid + 1
else:
r = mid -1
return l
def search_boundary_right_(nums, target):
l, r = 0, len(nums)
while l < r:
mid = l + (r - l) // 2
if nums[mid] == target:
l = mid + 1
elif nums[mid] < target:
l = mid + 1
else:
r = mid
return l
同理,如果想在target不在数组中时,返回-1,可以:
return l if l < len(nums) else -1
一个升序的有序数组,在某个点处经过旋转得到新的数组,然后在数组中搜索某个值的索引。
例:nums = [4, 5, 6, 0 , 1, 2, 3], target = 2
思路还是二分查找法,但是判断一个数在不在这个搜索区间内,只有当数组是有序的时候才容易判断,所以首先将一个旋转数组分成两半,肯定是一部分有序,另一部分无序,然后先判断哪边是有序的,哪边是无序的,如果是左边数组有序,然后判断target是不是在左边,否则就是在右边;如果是右边有序,先判断是不是在右边,否则就在左边。
def search_rotate(nums, target):
if len(nums) == 0:
return -1
l, r = 0, len(nums) - 1
while l <= r:
mid = l + (r - l) // 2
if nums[mid] == target:
return mid
if nums[0] <= nums[mid]:
if nums[0] <= target < nums[mid]:
r = mid-1
else:
l = mid+1
else:
if nums[mid] < target <= nums[-1]:
l = mid+1
else:
r = mid-1
return -1
方法:判断哪边有序,然后再看target是否在有序的部分,否则是无序部分
搜索插入位置用左闭右开区间比较方便,因为左右指针都指向对应的位置,并且统一。
def searchInsert(self, nums: List[int], target: int) -> int:
l, r = 0, len(nums)
while l < r:
mid = l + (r - l) // 2
if nums[mid] == target:
return mid
elif nums[mid] < target:
l = mid + 1
else:
r = mid
return l
def findPeakElement(nums):
l, r = 0, len(nums)-1
while l < r:
mid = l + (r - l)//2
if nums[mid] > nums[mid+1]:
r = mid
if nums[mid] < nums[mid+1]:
l = mid+1
return l
给定一个只包含整数的有序数组,每个元素都会出现两次,唯有一个数只会出现一次,找出这个数。
示例 1:
输入: [1,1,2,3,3,4,4,8,8] 输出: 2 示例 2:
输入: [3,3,7,7,10,11,11] 输出: 10
这题隐藏的信息是:出现一次的元素,左边的数字是成对出现的,因此其索引都是(偶数、奇数)成对出现,右边的元素则是(奇数、偶数)成对出现,如果发现mid左右的数字与mid所在的数字都不一样,那么返回nums[mid],否则在mid左右判断:如果成对出现的数字的索引是奇数在前,说明当前在单一元素的右边,搜索区间就往左跑,否则往右跑。
def search(nums):
# 注意这里初始化右边界为len(nums)-1,循环判断mid+1时数组越界
l, r = 0, len(nums) - 1
while l < r:
mid = l + (r - l) // 2
if nums[mid - 1] != nums[mid] and nums[mid] != nums[mid + 1]:
return nums[mid]
elif nums[mid - 1] == nums[mid]:
if (mid - 1) % 2 == 0:
l = mid + 1
else:
r = mid
elif nums[mid] == nums[mid+1]:
if mid % 2 == 0:
l = mid + 1
else:
r = mid
return nums[l]
4.寻找两个有序数组的中位数
33.搜索旋转排序数组
34.在排序数组中查找元素的第一个和最后一个位置
35.搜索插入位置
69.X的平方根
153寻找旋转排序数组中的最小值
162.寻找峰值
278.第一个错误版本
374.猜数字大小
540.有序数组中的单一元素
658.找到k个最接近的元素
704.二分查找
744.寻找比目标字母大的最小字母