【Leetcode 专题四】二分法

一、框架总结

总共四个框架

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

总结起来上面两种排除思路:

  1. 第一种是排除左区域,可行元素在mid及右边,此时end锁死右边界,然后不断往左缩小可行区域。此时适合找元素的左边界,因为每次搜索,可行区域都是[left, mid],此时只能找左边界。此时mid要下取整。
  2. 第二种是排除右区域, 可行元素在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

35. 搜索插入位置.
视频学习.

# 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

69. Sqrt(x).

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

367. 有效的完全平方数.

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

leetcode33. 搜索旋转排序数组.

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

81. 搜索旋转排序数组 II.

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. 寻找旋转排序数组中的最小值

类型:用排除法 思路二

153. 寻找旋转排序数组中的最小值.
在这里插入图片描述

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

类型:用排除法 思路二

154. 寻找旋转排序数组中的最小值 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. 搜索二维矩阵

类型:找某个值 思路一

74. 搜索二维矩阵.

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. 二维数组中的查找

类型:这道题我用的不是二分法,但是用二分法还是可以做的,以后会再写全。

剑指 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: 算法刷题重温(七): 二分查找.

猜你喜欢

转载自blog.csdn.net/qq_38253797/article/details/121170777