Leetcode打卡笔记4-二分查找


前言

学习了二分查找相关的知识点,它是一种折半查找算法,用于在有序数组中查找特定元素。基本步骤如下:
在这里插入图片描述
关于左右区间开闭问题,本次练习均采用左闭右闭区间,即left=0,right=len(nums)-1.这样可以简化代码考虑的情况.
关于mid的取值问题:
在这里插入图片描述
为了防止整型溢出问题,会采用以下的写法:
在这里插入图片描述

一、方法思路

这里简单介绍二分查找两种不同的实现思路。

1.直接法

直接法思想是在循环体中找到元素后直接返回结果。思路过程如下:
在这里插入图片描述
代码如下:

class Solution:
    def search(self, nums: List[int], target: int) -> int:
        left, right = 0, len(nums) - 1
        
        # 在区间 [left, right] 内查找 target
        while left <= right:
            # 取区间中间节点
            mid = left + (right - left) // 2
            # 如果找到目标值,则直接范围中心位置
            if nums[mid] == target:
                return mid
            # 如果 nums[mid] 小于目标值,则在 [mid + 1, right] 中继续搜索
            elif nums[mid] < target:
                left = mid + 1
            # 如果 nums[mid] 大于目标值,则在 [left, mid - 1] 中继续搜索
            else:
                right = mid - 1
        # 未搜索到元素,返回 -1
        return -1

这种方法适用于解决简单题目,针对查找元素简单,数组中都为非重复元素,且==、<、>的情况非常好写时。

2.排除法

排除法的思想是在循环体中排除目标元素不存在的区间。
基本思想如下:
在这里插入图片描述
针对这一思路,一共有两种不同的实现:

class Solution:
    def search(self, nums: List[int], target: int) -> int:
        left, right = 0, len(nums) - 1
        
        # 在区间 [left, right] 内查找 target
        while left < right:
            # 取区间中间节点
            mid = left + (right - left) // 2
            # nums[mid] 小于目标值,排除掉不可能区间 [left, mid],在 [mid + 1, right] 中继续搜索
            if nums[mid] < target:
                left = mid + 1 
            # nums[mid] 大于等于目标值,目标元素可能在 [left, mid] 中,在 [left, mid] 中继续搜索
            else:
                right = mid
        # 判断区间剩余元素是否为目标元素,不是则返回 -1
        return left if nums[left] == target else -1
class Solution:
    def search(self, nums: List[int], target: int) -> int:
        left, right = 0, len(nums) - 1
        
        # 在区间 [left, right] 内查找 target
        while left < right:
            # 取区间中间节点
            mid = left + (right - left + 1) // 2
            # nums[mid] 大于目标值,排除掉不可能区间 [mid, right],在 [left, mid - 1] 中继续搜索
            if nums[mid] > target:
                right = mid - 1 
            # nums[mid] 小于等于目标值,目标元素可能在 [mid, right] 中,在 [mid, right] 中继续搜索
            else:
                left = mid
        # 判断区间剩余元素是否为目标元素,不是则返回 -1
        return left if nums[left] == target else -1

由于终止条件使用的是left<right,所以退出循环时一定有left==right,不用再判断left和right哪个要返回。
总的逻辑就是优先排除不可能的区间,在剩余区间内查找元素。
特别要注意的是,为了避免陷入死循环,第二种情况mid应该向上取整,即mid = left + (right - left + 1) // 2。
两种方法的记忆方法是:第一种取的mid相对于中间位置偏左,所以left更新位置应该+1使其平衡,right正常等于mid;第二种取的mid相对于中间位置偏右,所以right更新位置应该-1使其平衡,left正常等于mid.
下面结合Leetcode具体题目进行探索。

二、Leetcode真题

1.二分查找

在这里插入图片描述
思路:二分查找入门级别题目,直接套用直接法的模板即可。

class Solution:
    def search(self, nums: List[int], target: int) -> int:
        left=0
        right=len(nums)-1
        while left<=right:
            mid=(left+right)//2
            if nums[mid]==target:
                return mid
            elif nums[mid]>target:
                right=mid-1
            else:
                left=mid+1
        return -1

2.搜索插入位置

在这里插入图片描述
仍然可以套用直接法的模板,不同的是目标值不存在,要返回插入位置,由直接法模板,可以判断出当跳出循环时有left>right,以数组[1,3,5,6]为例,若要求目标值为4,经过若干轮查找后,right=1,left=2.
显然4应在放在索引为2的位置,所以返回left的值。

class Solution:
    def searchInsert(self, nums: List[int], target: int) -> int:
        left=0
        right=len(nums)-1
        while left<=right:
            mid=(left+right)//2
            if nums[mid]==target:
                return mid
            elif nums[mid]<target:
                left=mid+1
            else:
                right=mid-1

        return left

3.猜数字大小

在这里插入图片描述
思路:读清楚题意,当取-1时,猜的数字过大,要取左半区间;当取1时,猜的数字过小,要取右半区间,再套用直接法模板即可。

class Solution:
    def guessNumber(self, n: int) -> int:
        left=1
        right=n
        while left<=right:
            mid=(left+right)//2
            ans=guess(mid)
            if ans==0:
                return mid
            elif ans==-1:
                right=mid-1
            else:
               left=mid+1
        return 0

4.x的平方根

在这里插入图片描述
思路:取左边界为0,右边界为整数x.套用直接法模板,不同的是,不采用判断中间值mid的if,直接涵盖在小于的情况里面(如果一直相等,则left会一直+1跳出循环),不断更新ans结果,最终返回。

class Solution:
    def mySqrt(self, x: int) -> int:
        left=0
        right=x
        ans=-1
        while left<=right:
            mid=(left+right)//2
            if mid*mid<=x:
                ans=mid
                left=mid+1
            else:
                right=mid-1
        return ans

5.两数之和II-输入有序数组

在这里插入图片描述
难度开始飙升,最直观的思路是使用暴力法求解:

class Solution:
    def twoSum(self, numbers: List[int], target: int) -> List[int]:
        size = len(numbers)
        for i in range(size):
            for j in range(i + 1, size):
                if numbers[i] + numbers[j] == target:
                    return [i + 1, j + 1]
        return [-1, -1]

但时间复杂度过高,为o(n^2),超出时间限制,这里考虑采用二分查找来减少时间复杂度。
具体做法如下:
在这里插入图片描述
这里采用排除法的模板去写

class Solution:
    def twoSum(self, numbers: List[int], target: int) -> List[int]:
        for i in range(len(numbers)):
            left, right = i + 1, len(numbers) - 1
            while left < right:
                mid = left + (right - left) // 2
                if numbers[mid] + numbers[i] < target:
                    left = mid + 1
                else:
                    right = mid
            if numbers[left] + numbers[i] == target:
                return [i + 1, left + 1]

        return [-1, -1]

这个方法的时间复杂度为o(nlogn)
使用双指针可以进一步减少时间复杂度到o(n),具体思路如下:
在这里插入图片描述
这一思路可以实现的原因是数组的升序排列的,所以可以缩小区间范围直到找到目标索引。判断条件要注意的是不能返回相同索引,所以跳出循环即认为没找到。

class Solution:
    def twoSum(self, numbers: List[int], target: int) -> List[int]:
        left = 0
        right = len(numbers) - 1
        while left < right:
            total = numbers[left] + numbers[right]
            if total == target:
                return [left + 1, right + 1]
            elif total < target:
                left += 1
            else:
                right -= 1
        return [-1, -1]

6.在D天内送到包裹的能力

在这里插入图片描述
思路:问题的关键在于明确二分查找的左右边界,这里指的最小运载能力下界应该是max(weights),因为要确保可以运到其中的单个货物,上界是sum(weights),满足题意。

class Solution:
    def shipWithinDays(self, weights: List[int], D: int) -> int:
        left = max(weights)
        right = sum(weights)

        while left < right:
            mid = (left + right) //2
            days = 1
            cur = 0
            for weight in weights:
                if cur + weight > mid:
                    days += 1
                    cur = 0
                cur += weight

            if days <= D:
                right = mid
            else:
                left = mid + 1
        return left

实现逻辑是,先初始化天数为1天,载货为0,不断遍历weights列表,增加载货与中间值进行判断,如果大于中间值,则超过载货能力,让载货清零,天数加1.
循环结束后,判断天数和D的关系,若小于等于,证明载货能力过大,取左半区间;否则取右半区间。

7.第一个错误的版本

在这里插入图片描述
思路:分析清楚题意,错误版本之后所有的版本都是错误的,是要找最初的错误版本。所以此次二分查找没有中间值判断,采用排除法的模板。当mid的版本为错误时,更新right为mid处,查找左半区间;否则更新left为mid+1处,查右半区间。退出循环时,返回left

class Solution:
    def firstBadVersion(self, n):
        left = 1
        right = n
        while left < right:
            mid = (left + right) // 2
            if isBadVersion(mid):
                right = mid
            else:
                left = mid + 1
        return left

8.搜索旋转排序数组

在这里插入图片描述
思路:旋转数组的特点就是可能会将升序数组打乱,但是从局部来看,无论从数组哪一个位置切分,剩下的两个数组都至少有一个为升序数组,这是此题的突破口。
先上代码:

class Solution:
    def search(self, nums: List[int], target: int) -> int:
        if not nums:
            return -1
        left,right=0,len(nums)-1
        while left<=right:
            mid=left+(right-left)//2
            if nums[mid]==target:
                return mid
            if nums[0]<=nums[mid]:
                if nums[0]<=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

代码基于直接法的思路。中间判断环节的逻辑是,当与中间值相等,就返回mid;再判断左半区间是否升序,若升序,如果目标值在区间中,则right左移;否则一定在右半区间中,left右移。
若左半区间不升序,则右半区间一定升序,再判断目标值是否在区间中,同样确定left和right的移动。

9.寻找旋转排序数组中的最小值

在这里插入图片描述
思路:数组性质与上题相同,直接定义两个指针left,right,如果中间值大于右边值,则最小值在右半区间,left=mid+1;否则最小值在左半区间,right=mid

class Solution:
    def findMin(self, nums: List[int]) -> int:
        left = 0
        right = len(nums) - 1
        while left < right:
            mid = left + (right - left) // 2
            if nums[mid] > nums[right]:
                left = mid + 1
            else:
                right = mid
        return nums[left]
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值