一、框架总结
总共四个框架
1.1、第一种思路(正向思维)及框架
思路讲解.
思想:假设元素在循环体,然后根据条件我们每次都搜索元素所在的那部分。这种思路可以称其为正向思维,也就是我只关注目标在哪。这种思路有两种写法。
1.1.1、第一种写法:假设target在一个闭区间[left, right]
这种写法是我们大多人学过的写法。思路是:在循环体(闭区间[left, right])里寻找元素。不过要注意:每次我们缩小搜索范围的时候,要始终保证target在一个闭区间才可以,这就是不变性。看代码感受下:
class Solution:
def searchInsert(self, nums: List[int], target: int) -> int:
# 异常判断
if len(nums) == 0: return -1
if nums[0] > target or nums[-1] < target : return -1
# 开始搜, 注意我们假定target在一个闭区间[left, right], 那么一开始的搜索范围肯定在这个闭区间中
left, right = 0, len(nums) - 1
# 这里要注意是小于等于,当left == right,区间[left , right]依然有效
while left <= right:
# 防止溢出现象 所以一般不写:(left+right)//2 还有一种优雅的写法:mid = left+ ((right - left) >> 1)
mid = left + (right - left) // 2
# 找到目标值了, 返回位置
if nums[mid] == target: return mid
# 说明target在mid左边,这时候搜索范围变为[left , mid-1], 始终保持闭区间搜索
elif nums[mid] > target: right = mid - 1
# 说明target在mid右边, 这时候搜索范围[mid+1, right ], 始终保持闭区间搜索
else: left = mid + 1
# 退出循环的时候,说明没有找到元素
return -1
1.1.2、第二种写法:假定target在一个闭区间[left, right)
思路:在循环体(开区间[left, right) )里寻找元素。
class Solution:
def searchInsert(self, nums: List[int], target: int) -> int:
# 异常判断
if len(nums) == 0: return -1
if nums[0] > target or nums[-1] < target : return -1
# 开始搜, 注意我们假定target在一个开区间[left, right), 那么一开始的搜索范围肯定在这个开区间中
left, right = 0, len(nums)
# 这里要注意是小于,没有等于,因为当left == right,区间[left , right)就没有效了
while left < right:
# 防止溢出现象 所以一般不写:(left+right)//2 还有一种优雅的写法:mid = left+ ((right - left) >> 1)
mid = left + (right - left) // 2
# 找到目标值了, 返回位置
if nums[mid] == target: return mid
# 说明target在mid左边,这时候搜索范围变为[left , mid) 始终保持开区间搜索
elif nums[mid] > target: right = mid
# 说明target在mid右边,这时候搜索范围变为[mid+1, right) 始终保持开区间搜索
else: left = mid+1
# 退出循环的时候,说明没有找到元素
return -1
注意:保证搜索区间的一致性,左闭右闭还是左闭右开先确定,然后循环的时候,这个区间始终保持住。用的较少。
1.2、第二种思路(逆向思维)及代码框架
思想:每一次都排除目前元素一定不在的那部分, 最后剩下的那个就是查找的元素。
思路讲解.
BiliBili: 视频讲解.
1.2.1、第一种写法
class Solution:
def searchInsert(self, nums: List[int], target: int) -> int:
# 异常判断
if len(nums) == 0: return -1
if nums[0] > target or nums[-1] < target : return -1
# 开始搜, 事先假定target在闭区间[begin, end]
left, right = 0, len(nums) - 1
# 开始二分查找,这里使用排除法, 先排除不可能的区间,那么看循环结束条件变了
# 注意此时不能有等于了,因为我们这里是排除不可能区间
# 那么当begin==end的时候,此时只剩下一个元素,要么是我们要的,要么不是,但此时要退出,不用再找,已经得到结果
while left < right:
mid = left + (right - left) // 2
# 这里不能判断等于的情况,因为我们用的排除思维,只需要排除目标一定不在的元素区间
# 此时说明目标元素一定不在mid及其左边,排除掉
if nums[mid] < target: left = mid + 1
# 这是nums[mid] >= target的情况,说明目标在mid及左边, 往左缩小
else: right = mid
# 退出循环,要么找到,要么没找到,如果找到的话,left和right都指向它了
return left if nums[left] == target else -1
1.2.2、第二种写法
class Solution:
def searchInsert(self, nums: List[int], target: int) -> int:
# 异常判断
if len(nums) == 0: return -1
if nums[0] > target or nums[-1] < target : return -1
# 开始搜, 事先假定target在闭区间[begin, end]
left, right = 0, len(nums) - 1
# 开始二分查找,这里使用排除法, 先排除不可能的区间,那么看循环结束条件变了
# 注意此时不能有等于了,因为我们这里是排除不可能区间
# 那么当begin==end的时候,此时只剩下一个元素,要么是我们要的,要么不是,但此时要退出,不用再找,已经得到结果
while left < right:
mid = left + (right - left + 1) // 2 # 注意这里要换成上取整
# 这里不能判断等于的情况,因为我们用的排除思维,只需要排除目标一定不在的元素区间
# 此时# 说明一定在左边了,此时排除掉mid及其右边
if nums[mid] > target: right = mid - 1
# 这是nums[mid] <= target的情况,# 说明在mid及其右边,所以排除掉左边
else: left = mid
# 退出循环,要么找到,要么没找到,如果找到的话,left和right都指向它了
return left if nums[left] == target else -1
总结起来上面两种排除思路:
- 第一种是排除左区域,可行元素在mid及右边,此时end锁死右边界,然后不断往左缩小可行区域。此时适合找元素的左边界,因为每次搜索,可行区域都是[left, mid],此时只能找左边界。此时mid要下取整。
- 第二种是排除右区域, 可行元素在mid及左边,此时begin锁死左边界,然后不断往右缩小可行区域,此时适合找元素的右边界,因为每次搜索,可行区域都是[mid, right],右边界在这里面。 此时mid要上取整。
1.3 思路总结
做这部分的题目的时候千万不要拿到题目就开始写题,也不要背上面的框架,以理解为主。拿到题的时候先分析该用哪种思路哪种框架,再开始写题。
- 思路1: 适合解决那种查找某个确定数值的题目, 如果二分查找问题简单,输入数组中不同元素个数只有1个,那么就使用思路1,而两个框架里面第一个会好想一点,毕竟之前就学过的。
- 思路2: 适合解决确定性边界的问题,当然确定数值也非常OK,只是用它来解决边界性问题比较好理解,如果二分查找问题比较复杂,要找一个可能在数组不存在或者边界问题,用思路2。 这两个框架的话建议都记住,灵活运用,毕竟有时候找左边界,有时候找右边界,锁定住的方式还不太一样。 拿到题目,首先判断是应该找左边界还是右边界,这时候后面的就好推了。
- 找左边界:往往会找到target本身或者大于target的最小值, 此时每一步排除左区间,end锁死右边,往左缩,mid下取整。
- 找右边界:往往会找到target本身或者小于target的最大值,此时每一步排除右区间,begin锁死左边,往右缩, mid上取整。
这里写了4个框架,最重要的其实是掌握3个框架,2个反向思维排除法框架和1个正向思维常用框架。
二、题型总结:完全有序
leetcode35. 搜索插入位置
类型:找某个位置,用思路1
# 1.1 正向思维 [left, right]
class Solution:
def searchInsert(self, nums: List[int], target: int) -> int:
# 开始搜, 注意我们假定target在一个闭区间[left, right], 那么一开始的搜索范围肯定在这个闭区间中
left, right = 0, len(nums) - 1
# 这里要注意是小于等于,当left == right,区间[left , right]依然有效
while left <= right:
# 防止溢出现象 所以一般不写:(left+right)//2 还有一种优雅的写法:mid = left+ ((right - left) >> 1)
mid = left + (right - left) // 2
# 找到目标值了, 返回位置
if nums[mid] == target: return mid
# 说明target在mid左边,这时候搜索范围变为[left , mid-1], 始终保持闭区间搜索
elif nums[mid] > target: right = mid - 1
# 说明target在mid右边, 这时候搜索范围[mid+1, right ], 始终保持闭区间搜索
else: left = mid + 1
# 退出循环的时候,说明没有找到元素,返回顺序插入的位置(可以画个图看看)
return left
# 1.2 正向思维 [left, right)
class Solution:
def searchInsert(self, nums: List[int], target: int) -> int:
# 开始搜, 注意我们假定target在一个开区间[left, right), 那么一开始的搜索范围肯定在这个开区间中
left, right = 0, len(nums)
# 这里要注意是小于,没有等于,因为当left == right,区间[left , right)就没有效了
while left < right:
# 防止溢出现象 所以一般不写:(left+right)//2 还有一种优雅的写法:mid = left+ ((right - left) >> 1)
mid = left + (right - left) // 2
# 找到目标值了, 返回位置
if nums[mid] == target: return mid
# 说明target在mid左边,这时候搜索范围变为[left , mid) 始终保持开区间搜索
elif nums[mid] > target: right = mid
# 说明target在mid右边,这时候搜索范围变为[mid+1, right) 始终保持开区间搜索
else: left = mid+1
# 退出循环的时候,说明没有找到元素,返回顺序插入的位置(可以画个图看看)
return -1
leetcode34. 在排序数组中查找元素的第一个和最后一个位置
类型:找左边界和有边界,用思路2
34. 在排序数组中查找元素的第一个和最后一个位置.
视频学习.
class Solution:
def searchRange(self, nums: List[int], target: int) -> List[int]:
res = [-1, -1]
if len(nums) == 0 or nums[0] > target or nums[-1] < target: return res
# 找左边界
left, right = 0, len(nums) - 1
while left < right:
mid = left + (right - left) // 2
if nums[mid] < target: left = mid + 1
else: right = mid
if nums[left] != target: return res
else: res[0] = left
# 找右边界
left, right = 0, len(nums) - 1
while left < right:
mid = left + (right - left + 1) // 2
if nums[mid] > target: right = mid - 1
else: left = mid
if nums[left] == target: res[-1] = left
return res
leetcode剑指 Offer 53 - I. 在排序数组中查找数字 I
类型:找左边界和有边界,用思路2
剑指 Offer 53 - I. 在排序数组中查找数字 I.
class Solution:
def search(self, nums: List[int], target: int) -> int:
if len(nums) == 0 or nums[0] > target or nums[-1] < target: return 0
# 找左边界
left, right = 0, len(nums) - 1
while left < right:
mid = left + (right - left) // 2
if nums[mid] < target: left = mid + 1
else: right = mid
if nums[left] != target: return 0
else: indexLeft = left
# 找右边界
left, right = 0, len(nums) - 1
while left < right:
mid = left + (right - left + 1) // 2
if nums[mid] > target: right = mid - 1
else: left = mid
if nums[left] == target: indexRight = left
return indexRight - indexLeft + 1
leetcode剑指 Offer 53 - II. 0~n-1中缺失的数字
类型:找某个位置,用思路1
剑指 Offer 53 - II. 0~n-1中缺失的数字.
class Solution:
def missingNumber(self, nums: List[int]) -> int:
left, right = 0, len(nums) - 1
while left <= right:
mid = left + (right - left) // 2
# 等于 说明左边数组没问题 再查找右边数组
if nums[mid] == mid: left = mid + 1
# 不等于 说明左边数组有问题 再查找左边数组
# 如果等于的话 会陷入无限循环中
# 情况1、假如mid不是缺省的值 但是nums[mid] != mid 说明mid之前有问题 right=mid-1
# 情况2、假如mid刚好是缺省的值 right=mid-1
# 看似会向左跳过这个缺省值 实际上后面left又会执行left=mid+1 跳出循环 返回left(缺省值)
else: right = mid - 1
return left
leetcode69. Sqrt(x)
类型:找右边界,思路2
class Solution:
def mySqrt(self, x: int) -> int:
left, right = 0, x // 2 + 1
while left < right:
mid = left + (right - left + 1) // 2
if mid * mid > x: right = mid - 1
else: left = mid
return left
leetcode367. 有效的完全平方数
类型:找某个位置,用思路1
class Solution:
def isPerfectSquare(self, num: int) -> bool:
left, right = 0, num / 2 + 1
while left <= right:
mid = left + (right - left) // 2
if mid * mid == num: return True
elif mid * mid > num: right = mid - 1
else: left = mid + 1
return False
三、题型总结:不完全有序
旋转数组(旋转之后, 其中一部分肯定是有序的,一部分是无序的)搜索系列。只要题目给出的数组是有序的,我们的第一想法就应该是使用二分法搜索,虽然旋转数组是不完全有序的,我们定出了mid之后,虽然不能再根据nums[mid]和target的值来确定下一个搜索区间(left或者right的值),但是我们仍然可以使用二分法, 思路:
- 因为旋转数组是一部分有序一部分无序的,也就是说确定了mid之后,mid的左边和右边是一个有序和一个无序的,所以我们确定了mid之后,应该马上确定mid的那一边是有序的,哪一边是无序的,再来确定下一个搜索空间(left和right的值)。
leetcode33. 搜索旋转排序数组
类型:找某个位置,用思路1
class Solution:
def search(self, nums: List[int], target: int) -> int:
left, right = 0, len(nums) - 1
# 正向思维 框架1.1
while left <= right:
mid = left + (right - left) // 2
if nums[mid] == target: return mid # 找到了
elif nums[left] <= nums[mid]: # 左边有序 =成立时左边只要一个元素
# 注意这个等号 搜索空间应该是[left, mid-1] 右边的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
leetcode81. 搜索旋转排序数组 II
类型:找某个位置,用思路1
class Solution:
def search(self, nums: List[int], target: int) -> bool:
left, right = 0, len(nums) - 1
while left <= right:
mid = left + (right - left) // 2
if nums[mid] == target: return True
# 存在重复元素的话,那么如果我们nums[left]==nums[mid] 就判断不了左边是否有序的
# 比如 13111 lef=1 mid=1 target=3 如果left=1=mid 跳过左半边 target刚好跳过 错了
# 将重复的元素去掉
if nums[left] == nums[mid]:
left += 1
continue
if nums[left] < nums[mid]: # 左边有序
if nums[left] <= target < nums[mid]: # target可能在左边
right = mid - 1
else: # target可能在右边
left = mid + 1
else: # 右边有序
if nums[mid] < target <= nums[right]:
left = mid + 1
else:
right = mid - 1
return False
leetcode153. 寻找旋转排序数组中的最小值
类型:用排除法 思路二
class Solution:
def findMin(self, nums: List[int]) -> int:
left, right = 0, len(nums) - 1
# 使用思路二:排除法 每一步都排除最小值不可能的区间 到终止的时候 left==right 就找到了最小值了
while left < right:
mid = left + (right - left) // 2
if nums[mid] > nums[right]: #右边无序 说明左边有序 且最小值一定不在左边
left = mid + 1
else: # 右边有序 最小值一定在左边+mid
right = mid
return nums[left]
leetcode154. 寻找旋转排序数组中的最小值 II
类型:用排除法 思路二
class Solution:
def findMin(self, nums: List[int]) -> int:
left, right = 0, len(nums) - 1
while left < right:
mid = left + (right - left) // 2
if nums[mid] == nums[right]:
right -= 1
continue
if nums[mid] > nums[right]:
left = mid + 1
else:
right = mid
return nums[left]
同样用这种思路还可以求升序的最大值,降序的最小值最大值等,思想核心都一样。讲解: https://zhongqiang.blog.csdn.net/article/details/114519435.
四、题型总结:二维数组
leetcode74. 搜索二维矩阵
类型:找某个值 思路一
class Solution:
def searchMatrix(self, matrix: List[List[int]], target: int) -> bool:
# 将二位坐标转换为一维坐标
row, col = len(matrix), len(matrix[0])
left, right = 0, row * col - 1
while left <= right:
mid = left + (right - left) // 2
# 将一维坐标转换为二维坐标
if matrix[mid // col][mid % col] == target: return True
elif matrix[mid // col][mid % col] < target: left = mid + 1
else: right = mid - 1
return False
leetcode剑指 Offer 04. 二维数组中的查找
类型:这道题我用的不是二分法,但是用二分法还是可以做的,以后会再写全。
class Solution:
def findNumberIn2DArray(self, matrix: List[List[int]], target: int) -> bool:
row, col = len(matrix) - 1, 0
while row >= 0 and col < len(matrix[0]):
if matrix[row][col] == target: return True
elif matrix[row][col] > target: row -= 1
else: col += 1
return False
Reference
b站视频: 二分查找(Binary Search)合集.
CSDN: 算法刷题重温(七): 二分查找.