二分查找——题目详解(278、33、4、875)

278、第一个错误版本

        有n个版本,第一个错误版本和之后的所有错误版本调用isbadversion都会返回true 之前的版本返回flase 。需要找到第一个错误版本

        很自然地想到二分查找,不然就是遍历数组从前往后查。

        在本题需要注意的是,如果m是错误版本,则需要变更右边界,但是当前的m可能就是答案,所有不能r=m+1,需要写成r=m。而左边界则因为排除掉m了,l=m+1。

        循环条件l<r,因此结束循环是l==r,直接输入l。

class Solution:
    def firstBadVersion(self, n: int) -> int:
        l,r=1,n
        while(l<r):
            m=(l+r)//2
            flag=isBadVersion(m)
            if not flag:
                l=m+1
            elif flag:
                r=m

        return l

33、搜索旋转排序数组

        有个数组nums 原本是升序,但是有k+1一个移到了数组前面,因此数组被分为两个有序数组[0,k]和[k+1,len(nums)-1],分别使用二分查找

class Solution:
    def search(self, nums: List[int], target: int) -> int:
        k=0
        #寻找旋转了多少个数,k
        for i in range (len(nums)-1):
            if  nums[i]>nums[i+1]:
                k=i
                break

        def binarysearch(l,r,nums,target):
            while(l<=r):
                m=(l+r)//2
                if nums[m]==target:
                    return m
                elif nums[m]<target:
                    l=m+1
                else :
                    r=m-1
            return -1

        ans1=binarysearch(0,k,nums,target)
        ans2=binarysearch(k+1,len(nums)-1,nums,target)  
        if ans1!=-1:
            return ans1
        elif ans2!=-1:
            return ans2
        else:
            return -1

153、154寻找旋转排序数组中的最小数

        思路都一样,题目的区别是154有重复的数。直接找出两个数组的位置,返回最小值即可.

使用二分法的思路见官方详解。

class Solution:
    def findMin(self, nums: List[int]) -> int:
        k=0
        #寻找旋转了多少个数,k
        for i in range (len(nums)-1):
            if  nums[i]>nums[i+1]:
                k=i
                break

        def findmin(l,r,nums):
            if l<=len(nums)-1:
                return nums[l]
            else: 
                return 999999

        ans1=findmin(0,k,nums)
        ans2=findmin(k+1,len(nums)-1,nums)  
        return min(ans1,ans2)  

4、寻找两个正序数组的中位数

        有两个正序数组,需要找出它们合并之后的中位数,时间要求O(log(m+n))。看到这个要求可以想到二分或者是堆。

        接下来分析如何进行二分查找:找出中位数即找出数组中间的一个或两个数。当长度为奇数时是找出第(m+n+1)//2个数,为偶数是找出第(m+n)//2和(m+n)//2+1,求平均值。分别使用索引idx1和idx2划分nums1和nums2,使得数组的左侧有(m+n+1)//2个数——条件1,以及左侧的数均小于右侧的数——条件二

        因此遍历区间是[0,m]

        变化条件是 条件二左侧的数不是均小于右侧的数:

                nums1[idx1]<nums2[idx2-1]:

                nums1[idx1-1]>nums2[idx2]

        当满足条件一和条件二之后跳出循环,返回值。需要注意边界条件和数组总长度是否为偶数。

        如果为奇数,只需要返回maxleft,此时不考虑数组越界(边界)的话,直接比较nums[idx1-1]、nums[idx2-1]的大小。但是需要考虑idx1或idx2为0,也就是nums全在中位数的右侧的情况。

        如果为奇数,还需要考虑中位数右侧的最小值,同样地需要考虑边界条件。又因为一开始做了交换,使得nums1永远是长度较短的数组,所以代码中不需要考虑idx2==n。

        如果有不清楚的地方还可以参考哔站视频:讲得非常简单易懂,而且也有代码讲解LeetCode004-两个有序数组的中位数-最优算法代码讲解_哔哩哔哩_bilibili

        最后代码如下:

class Solution:
    def findMedianSortedArrays(self, nums1: List[int], nums2: List[int]) -> float:
        m,n=len(nums1),len(nums2)
        if m>n :
            nums1,nums2=nums2,nums1
            m,n=n,m
        l,r=0,m
        while l<=r:
            #保证条件1
            idx1=(l+r)//2
            idx2=(m+n+1)//2-idx1
            #保证条件2
            if idx1!=m and idx2!=0 and nums1[idx1]<nums2[idx2-1]:
                l=idx1+1
            elif idx1!=0 and nums1[idx1-1]>nums2[idx2]:
                r=idx1-1
            #条件都满足退出循环
            else:
                break
        #考虑边界
        if idx1==0:
            maxleft=nums2[idx2-1]
        elif idx2==0:
            maxleft=nums1[idx1-1]
        else:
            maxleft=max(nums2[idx2-1],nums1[idx1-1])
        #长度为奇数
        if (m+n)%2==1:
             return maxleft
        #长度为偶数
        else:
            if idx1==m:
                minright=nums2[idx2]
            elif idx2==n:
                minright=nums1[idx1]
            else:
                minright=min(nums1[idx1],nums2[idx2])
            return (maxleft+minright)/2.0

875、爱吃香蕉的珂珂

与前面D天运送包裹的题目类似,先确定遍历区间每小时最少吃多少香蕉,和最多吃多少香蕉。

然后要注意的是向上取整的写法:

hour += (num + mid - 1) // mid  # 等价于向上取整

一开始我写的是hour+=num//mid+1,忽略了num是mid的整数倍的情况

class Solution:
    def minEatingSpeed(self, piles: List[int], h: int) -> int:
        #确定区间
        l=1
        r=max(piles)
        
        while l<=r:
            hour=0
            mid=(l+r)//2
            for num in piles:
                if num<=mid:
                    hour+=1
                else:
                    hour=hour+(num+mid-1)//mid
            if hour>h:
                l=mid+1
            else:
                r=mid-1
        return l

410、分割最大数组

        给定一个数组,需要将数组分为k个非空的连续子数组,使得这k个子数组和的最大值最小。

比如nums=[7,2,5,10,8],k=2 应该分成[7,2,5]、[10,8] 输出18

        如何使用二分法划分:

        确定划分区间,也就是子数组和的最小值和最大值,其实和之前的题差不多

                l = max(nums)  # 最小可能的最大子数组和(至少为数组中的最大值)

                r = sum(nums)  # 最大可能的最大子数组和(即不分割时的总和)

        进行比较变更搜索区间

        感觉这题的难点可能在这,一开始做的时候我想先划分符合要求的子数组,也就是划分成k个,比较子数组的和的最大值和mid然后进行区间变化。但是这样子想复杂了。应该直接用mid限制子数组的最大值,进行划分,划分后直接比较子数组数count和要求的数量k。

        

class Solution:
    def splitArray(self, nums: list[int], k: int) -> int:
        # 确定二分查找的区间
        l = max(nums)  # 最小可能的最大子数组和(至少为数组中的最大值)
        r = sum(nums)  # 最大可能的最大子数组和(即不分割时的总和)

        # 二分查找
        while l < r:
            mid = (l + r) // 2  # 当前尝试的最大子数组和
            count = 1  # 当前分割的子数组数量
            current_sum = 0  # 当前子数组的和

            # 模拟分割过程
            for num in nums:
                if current_sum + num > mid:  # 当前子数组的和超过 mid,开始新的子数组
                    count += 1
                    current_sum = num
                else:
                    current_sum += num

            # 根据分割结果调整搜索范围
            if count <= k:
                r = mid  # 分割成功,尝试更小的最大子数组和
            else:
                l = mid + 1  # 分割失败,尝试更大的最大子数组和

        # 返回最小满足条件的最大子数组和
        return l

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值