导语
前面的笔记基本将Leetcode上主要的内容进行了一刷,现在进行回顾总结,本文主要介绍几个二分搜索相关的题目,并进行总结整理。
所涉及的题目如下:
| 题目列表 | 难度 |
|---|---|
| ★704. 二分查找 | 简单 |
| 35. 搜索插入位置 | 简单 |
| 278. 第一个错误的版本 | 简单 |
| 367. 有效的完全平方数 | 简单 |
| ★34. 在排序数组中查找元素的第一个和最后一个位置 | 中等 |
| 33. 搜索旋转排序数组 | 中等 |
| 81. 搜索旋转排序数组 II | 中等 |
704. 二分查找
本题目是上面所有相关题目的一个基础,代码整体也比较简单模板化,直接记住即可。这里所有的题目我都是左闭右闭区间来记忆的,避免混淆。
class Solution:
def search(self, nums: List[int], target: int) -> int:
# 双指针,对撞指针
left, right = 0, len(nums)-1
while left<=right:
mid = (left + right) // 2
if nums[mid] < target:
left = mid + 1
elif nums[mid] > target:
right = mid - 1
else:
return mid
return -1
35. 搜索插入位置
这道题目的本质等价于查找第一个大于目标值的数,可变形为查找最后一个不大于目标值的数(-1即可)。实际上,当上面的二分查找代码找不到目标值跳出时,一定有 且left及其右侧的数字都比target大,right及其左侧的数字都比target小。
所以,寻找插入位置,实质上就是left的位置。
class Solution:
def searchInsert(self, nums: List[int], target: int) -> int:
# 初始化左右指针
left, right = 0, len(nums) - 1
# 当左右指针没有交错时继续循环
while left <= right:
# 计算中间索引,用于分割数组
mid = (right - left) // 2 + left
# 如果找到了目标值,直接返回其索引
if nums[mid] == target:
return mid
# 如果中间值小于目标值,说明目标值在[mid+1, right]之间
# 更新left为mid+1
elif nums[mid] < target:
left = mid + 1
# 如果中间值大于目标值,说明目标值在[left, mid-1]之间
# 更新right为mid-1
else:
right = mid - 1
# 如果循环结束还没有返回,说明目标值不在数组中
# 此时left指向的是第一个大于target的数的索引,
# 或者是数组长度(如果target大于数组中的所有数)
# 所以返回left即可
return left
278. 第一个错误的版本
这道题目其实可以抽象为在一个有序的01数组中,寻找第一个1的位置,即寻找给定target的左边界(更详细的讨论可以参考下面的34. 在排序数组中查找元素的第一个和最后一个位置)。isBadVersion(mid)实际上就等价于nums[mid]>=target:
最后,只要return left即可。
# The isBadVersion API is already defined for you.
# def isBadVersion(version: int) -> bool:
class Solution:
def firstBadVersion(self, n: int) -> int:
left, right = 1, n
ans = n
while right>=left:
mid = (right+left)//2
if isBadVersion(mid):
right = mid - 1
ans = mid
else:
left = mid + 1
return ans
367. 有效的完全平方数
这道题目就是简单应用一下二分查找,由于不让使用库函数,我们直接从1,n开始进行二分查找,对比nums[i]*nums[i]与target的值即可。
class Solution:
def isPerfectSquare(self, num: int) -> bool:
left, right = 1, num
while left <= right:
mid = (left+right)//2
result = mid * mid
if result>num:
right = mid - 1
elif result<num:
left = mid + 1
else:
return True
return False
34. 在排序数组中查找元素的第一个和最后一个位置
这道题目就是有重复数字情况下的二分查找,分别查找左右边界即可,这里还是使用左闭右闭的区间形式,查找代码如下:
class Solution:
def searchRange(self, nums: List[int], target: int) -> List[int]:
def searchLeft(nums, target):
left, right = 0, len(nums)-1
while left <= right:
mid = (right-left)//2 + left
if nums[mid] >= target:
right = mid - 1
else:
left = mid + 1
return left
def searchRight(nums, target):
left, right = 0, len(nums)-1
while left <= right:
mid = (right-left)//2 + left
if nums[mid] <= target:
left = mid + 1
else:
right = mid - 1
return right
left, right = searchLeft(nums, target), searchRight(nums, target)
if left == len(nums) or nums[left] != target:
return [-1, -1]
else:
return [left, right]
这里可以简单的记:
- 查找左边界时,遇到大于等于(即原本大于的情形)更新右指针right,最终返回左指针left;
- 查找右边界时,遇到小于等于(即原本小于的情形)更新左指针left,最终返回右指针right。
也就是说,找左边界时,将等于的情况放到右边界的处理逻辑中,返回左边界;找右边界时,将等于的情况放到左边的处理逻辑中,返回右边界。
33. 搜索旋转排序数组
题目描述
整数数组
nums按升序排列,数组中的值 互不相同 。在传递给函数之前,nums在预先未知的某个下标k(0 <= k < nums.length)上进行了 旋转,使数组变为[nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]](下标 从 0 开始 计数)。例如,[0,1,2,4,5,6,7]在下标3处经旋转后可能变为[4,5,6,7,0,1,2]。 给你 旋转后 的数组nums和一个整数target,如果nums中存在这个目标值target,则返回它的下标,否则返回-1。你必须设计一个时间复杂度为O(log n)的算法解决此问题。
示例 1:
输入: nums = [4,5,6,7,0,1,2], target = 0
输出: 4
提示:
1 <= nums.length <= 5000-104 <= nums[i] <= 104nums中的每个值都 独一无二- 题目数据保证
nums在预先未知的某个下标上进行了旋转 -104 <= target <= 104
解法
这个题目的巧妙之处在于尽管旋转后不是一个单调递增的数组,但是假如我们使用二分法计算一个mid,那么它的某一侧,一定是单调递增的,那么我们就可以在这一侧进行二分搜索。这里可以参考Leetcode官方题解的一个图片:
所以,整体代码只需要判断处理target是不是在单调递增的那一侧,如果是的话,将区间放到这一侧上,否则放到另一侧(又是和一开始一样的情况:有一侧是单调的)。
完整代码如下:
class Solution:
def search(self, nums: List[int], target: int) -> int:
left, right = 0, len(nums)-1
while left <= right:
mid = (left + right) // 2
if nums[mid] == target:
return mid
if nums[left] <= nums[mid]:
if nums[left] <= target < nums[mid]:
right = mid - 1
else:
left = mid + 1
else:
if nums[mid] < target <= nums[right]:
left = mid + 1
else:
right = mid - 1
return -1
81. 搜索旋转排序数组 II
题目描述
已知存在一个按非降序排列的整数数组
nums,数组中的值不必互不相同。在传递给函数之前,nums在预先未知的某个下标k(0 <= k < nums.length)上进行了 旋转 ,使数组变为[nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]](下标 从 0 开始 计数)。例如,[0,1,2,4,4,4,5,6,6,7]在下标5处经旋转后可能变为[4,5,6,6,7,0,1,2,4,4]。给你 旋转后 的数组nums和一个整数target,请你编写一个函数来判断给定的目标值是否存在于数组中。如果nums中存在这个目标值target,则返回true,否则返回false。 你必须尽可能减少整个操作步骤。
示例 1:
输入: nums = [2,5,6,0,0,1,2], target = 0
输出: true
提示:
1 <= nums.length <= 5000-104 <= nums[i] <= 104- 题目数据保证
nums在预先未知的某个下标上进行了旋转 -104 <= target <= 104
进阶:: 这是 搜索旋转排序数组 的延伸题目,本题中的
nums可能包含重复元素。这会影响到程序的时间复杂度吗?会有怎样的影响,为什么?
解法
这道题目如果套用上一道题目的解法,会发现有的测试样例无法通过。这是因为,之前一定是从一个单调递增的地方旋转的,而这里有可能是在两个相同值的元素之间的位置旋转的,所以,只需要在上题基础之上,单独处理一下这种情况即可:
class Solution:
def search(self, nums: List[int], target: int) -> bool:
left, right = 0, len(nums)-1
while left <= right:
mid = (left + right) // 2
if nums[mid] == target:
return True
# 如果左值等于中值,可能无法确定区间是否有序
if nums[left] == nums[mid]:
left += 1
continue
if nums[left] <= nums[mid]:
if nums[left] <= target < nums[mid]:
right = mid - 1
else:
left = mid + 1
else:
if nums[mid] < target <= nums[right]:
left = mid + 1
else:
right = mid - 1
return False
总结
经典的二分搜索代码是需要在理解的基础上进行记忆的,后续有重复元素的情况的左右边界也需要记忆,其他无非是在此基础上的一些变形,灵活运用即可。