Leetcode刷题总结(一):二分查找

88 阅读4分钟

导语

前面的笔记基本将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=right+1left = right + 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]

这里可以简单的记:

  1. 查找左边界时,遇到大于等于(即原本大于的情形)更新右指针right,最终返回左指针left;
  2. 查找右边界时,遇到小于等于(即原本小于的情形)更新左指针left,最终返回右指针right。

也就是说,找左边界时,将等于的情况放到右边界的处理逻辑中,返回左边界;找右边界时,将等于的情况放到左边的处理逻辑中,返回右边界。

33. 搜索旋转排序数组

题目描述

整数数组 nums 按升序排列,数组中的值 互不相同 。在传递给函数之前,nums 在预先未知的某个下标 k0 <= 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] <= 104
  • nums 中的每个值都 独一无二
  • 题目数据保证 nums 在预先未知的某个下标上进行了旋转
  • -104 <= target <= 104

解法

这个题目的巧妙之处在于尽管旋转后不是一个单调递增的数组,但是假如我们使用二分法计算一个mid,那么它的某一侧,一定是单调递增的,那么我们就可以在这一侧进行二分搜索。这里可以参考Leetcode官方题解的一个图片:

image.png

所以,整体代码只需要判断处理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 在预先未知的某个下标 k0 <= 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

总结

经典的二分搜索代码是需要在理解的基础上进行记忆的,后续有重复元素的情况的左右边界也需要记忆,其他无非是在此基础上的一些变形,灵活运用即可。