leetcode笔记: 二分查找

这篇博客探讨了如何利用二分查找算法解决不同信息技术问题,包括在旋转数组中寻找目标值、寻找最小元素、判断目标值是否存在、优化工作分配和找出错误版本等问题。通过二分查找,可以在对数时间内高效地解决这些挑战。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

33
整数数组 nums 按升序排列,数组中的值 互不相同 。

在传递给函数之前,nums 在预先未知的某个下标 k(0 <= 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 。

示例 1:
输入: nums = [4,5,6,7,0,1,2], target = 0
输出: 4

示例2:
输入: nums = [4,5,6,7,0,1,2], target = 3
输出: -1

# 题目要求时间复杂度为logn,因此基本就是二分法了。 这道题目不是直接的有序数组,不然就是easy了。
# 首先要知道,我们随便选择一个点,将数组分为前后两部分,其中一部分一定是有序的。
#
# 具体步骤:
# 我们可以先找出mid,然后根据mid来判断,mid是在有序的部分还是无序的部分
# 假如mid小于start,则mid一定在右边有序部分。
# 假如mid大于等于start, 则mid一定在左边有序部分。

# 我们只需要比较target和有序部分的边界关系就行了。 比如mid在右侧有序部分,即[mid, end]
# 那么我们只需要判断 target >= mid && target <= end 就能知道target在右侧有序部分,我们就
# 可以舍弃左边部分了(start = mid + 1), 反之亦然。
class Solution:
    def search(self, nums: List[int], target: int) -> int:
        n = len(nums)
        if n <= 0:
            return -1

        left = 0
        right = n - 1
        while left < right:
            mid = (right - left) // 2 + left
            if nums[mid] == target:
                return mid

            # 找到顺序的序列范围
            if nums[mid] > nums[left]:  # 如果中间的值大于最左边的值,说明左边有序
                if nums[left] <= target <= nums[mid]:
                    right = mid
                else:
                    # 这里 +1,因为上面是 <= 符号
                    left = mid + 1
            # 否则右边有序
            else:
                # 注意:这里必须是 mid+1,因为根据我们的比较方式,mid属于左边的序列
                if nums[mid + 1] <= target <= nums[right]:
                    left = mid + 1
                else:
                    right = mid

        return left if nums[left] == target else -1


81
已知存在一个按非降序排列的整数数组 nums ,数组中的值不必互不相同。

在传递给函数之前,nums 在预先未知的某个下标 k(0 <= 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

示例2:
输入:nums = [2,5,6,0,0,1,2], target = 3
输出:false

# 二分查找
# 对于数组中有重复元素的情况,二分查找时可能会有 a[l]=a[mid]=a[r],此时无法判断区间 [l,mid] 和区间 [mid+1,r] 哪个是有序的。
# 对于这种情况,我们只能将当前二分区间的左边界加一,右边界减一,然后在新区间上继续二分查找
class Solution:
    def search(self, nums: List[int], target: int) -> bool:
        if not nums:
            return False

        n = len(nums)
        if n == 1:
            return nums[0] == target

        l, r = 0, n - 1
        while l <= r:
            mid = (l + r) // 2
            if nums[mid] == target:
                return True
            if nums[l] == nums[mid] and nums[mid] == nums[r]:
                l += 1
                r -= 1
            elif nums[l] <= nums[mid]:
                if nums[l] <= target and target < nums[mid]:
                    r = mid - 1
                else:
                    l = mid + 1
            else:
                if nums[mid] < target and target <= nums[n - 1]:
                    l = mid + 1
                else:
                    r = mid - 1

        return False


153
假设按照升序排序的数组在预先未知的某个点上进行了旋转。

( 例如,数组[0,1,2,4,5,6,7] 可能变为[4,5,6,7,0,1,2])。

请找出其中最小的元素。
你可以假设数组中不存在重复元素。

示例 1:
输入: [3,4,5,1,2]
输出: 1

示例 2:
输入: [4,5,6,7,0,1,2]
输出: 0

# 二分
class Solution:
    def findMin(self, nums: List[int]) -> int:
        left, right = 0, len(nums) - 1

        while left < right:
            mid = (left + right) // 2

            if nums[mid] > nums[right]:  # 说明 nums[mid] 是最小值左侧的元素, 因此我们可以忽略二分查找区间的左半部分
                left = mid + 1
            elif nums[mid] < nums[right]:  # 说明 nums[mid] 是最小值右侧的元素, 因此我们可以忽略二分查找区间的右半部分
                right = mid

        return nums[left]


154
假设按照升序排序的数组在预先未知的某个点上进行了旋转。

( 例如,数组[0,1,2,4,5,6,7] 可能变为[4,5,6,7,0,1,2])。

请找出其中最小的元素。
注意数组中可能存在重复的元素。

示例 1:
输入: [1,3,5]
输出: 1

示例2:
输入: [2,2,2,0,1]
输出: 0

    def findMin(self, nums: List[int]) -> int:
        left, right = 0, len(nums) - 1

        while left < right:
            mid = (left + right) // 2

            if nums[mid] > nums[right]:
                left = mid + 1
            elif nums[mid] < nums[right]:
                right = mid
            else:  # 出现相等的情况
                # 由于它们的值相同,所以无论 nums[high] 是不是最小值,都有一个它的「替代品」nums[pivot],因此我们可以忽略二分查找区间的右端点
                right -= 1

        return nums[left]


1723
给你一个整数数组 jobs ,其中 jobs[i] 是完成第 i 项工作要花费的时间。

请你将这些工作分配给 k 位工人。所有工作都应该分配给工人,且每项工作只能分配给一位工人。
工人的 工作时间 是完成分配给他们的所有工作花费时间的总和。请你设计一套最佳的工作分配方案,使工人的 最大工作时间 得以 最小化 。

返回分配方案中尽可能 最小 的 最大工作时间 。

示例 1:
输入:jobs = [3,2,3], k = 3
输出:3
解释:给每位工人分配一项工作,最大工作时间是 3 。

示例 2:
输入:jobs = [1,2,4,7,8], k = 2
输出:11
解释:按下述方式分配工作:
1 号工人:1、2、8(工作时间 = 1 + 2 + 8 = 11)
2 号工人:4、7(工作时间 = 4 + 7 = 11)
最大工作时间是 11 。

# 二分 + 递归 + 剪枝
# 参考 1011
# 我们想象一下 K 个人,可以设置一个每个人最大运输量大小为limit,如果在这个limit下,
# 工作能分完,这个方案是可行的,如果在这个limit 下分不完,那这个方案不可行。
class Solution:
    def minimumTimeRequired(self, jobs: List[int], k: int) -> int:
        def check(limit):  # 检查这个limit行不行
            arr = sorted(jobs)  # 小优化,排序后,大的先拿出来试
            groups = [0] * k
            # 分成 K 组,看看在这个limit 下 能不能安排完工作
            if backtrace(arr, groups, limit):
                return True
            else:
                return False

        def backtrace(arr, groups, limit):
            # 尝试每种可能性
            if not arr:
                return True  # 分完,则方案可行
            v = arr.pop()  # 取出最大的工作
            for i in range(len(groups)):
                if groups[i] + v <= limit:
                    groups[i] += v
                    if backtrace(arr, groups, limit):  # 递归
                        return True
                    groups[i] -= v  # 上一步不是true话, 回溯, 接着递归
                    # 剪枝,如果这个工人没分到活,那别人肯定得多干活了,那最后的结果必然不是最小的最大值,就不用继续试了。
                    if groups[i] == 0:
                        break
            arr.append(v)  # 可以不加, 但上面递归行要改成 if backtrace(arr.copy(), groups, limit)
            return False

        # 每个人承担的工作的上限,最小为 job 里面的最大值,最大为 jobs 之和
        left, right = max(jobs), sum(jobs)
        while left < right:  # 开始二分查找
            mid = (left + right) // 2
            if check(mid):  # 可以完成工作
                right = mid
            else:  # 不能完成
                left = mid + 1
        return left


1482
给你一个整数数组 bloomDay,以及两个整数 m 和 k 。
现需要制作 m 束花。制作花束时,每束需要使用花园中 相邻的 k 朵花 。
花园中有 n 朵花,第 i 朵花会在 bloomDay[i] 时盛开,恰好 可以用于 一束 花中。

请你返回从花园中摘 m 束花需要等待的最少的天数。如果不能摘到 m 束花则返回 -1 。

示例 1:
输入:bloomDay = [1,10,3,10,2], m = 3, k = 1
输出:3
解释:让我们一起观察这三天的花开过程,x 表示花开,而 _ 表示花还未开。
现在需要制作 3 束花,每束只需要 1 朵。
1 天后:[x, _, _, _, _] // 只能制作 1 束花
2 天后:[x, _, _, _, x] // 只能制作 2 束花
3 天后:[x, _, x, _, x] // 可以制作 3 束花,答案为 3

示例 2:
输入:bloomDay = [1,10,3,10,2], m = 3, k = 2
输出:-1
解释:要制作 3 束花,每束需要 2 朵花,也就是一共需要 6 朵花。而花园中只有 5 朵花,无法满足制作要求,返回 -1 。

示例 3:
输入:bloomDay = [7,7,7,7,12,7,7], m = 2, k = 3
输出:12
解释:要制作 2 束花,每束需要 3 朵。
花园在 7 天后和 12 天后的情况如下:
7 天后:[x, x, x, x, _, x, x]
可以用前 3 朵盛开的花制作第一束花。但不能使用后 3 朵盛开的花,因为它们不相邻。
12 天后:[x, x, x, x, x, x, x]
显然,我们可以用不同的方式制作两束花。

示例 4:
输入:bloomDay = [1000000000,1000000000], m = 1, k = 1
输出:1000000000
解释:需要等 1000000000 天才能采到花来制作花束

示例 5:
输入:bloomDay = [1,10,2,9,3,8,4,7,5,6], m = 4, k = 2
输出:9

# 二分
class Solution:
    def minDays(self, bloomDay: List[int], m: int, k: int) -> int:
        # 最好用除法, 因为10 ^ 5 * 10 ^ 6 = 10 ^ 11 > 2, 147, 483, 647, 会越界
        if m > len(bloomDay) / k:  # 花的数量不够
            return -1

        def canMake(target_days: int) -> bool:
            bouquets = flowers = 0  # 分别为 目前制作的花束数量 和 连续花朵数量
            for day in bloomDay:
                if day <= target_days:  # 此花能及时开
                    flowers += 1
                    if flowers == k:  # 制作1束花
                        bouquets += 1
                        if bouquets == m:  # 满足题目要求
                            return True
                        flowers = 0
                else:  # 每束花的每朵花必须相连, 如中断则重新计数
                    flowers = 0
            return bouquets == m

        left, right = min(bloomDay), max(bloomDay)  # 左右指针
        while left < right:  # 开始二分查找
            target_days = (left + right) // 2  # 找到最小的天数
            if canMake(target_days):  # 此函数检查天数够不够
                right = target_days
            else:
                left = target_days + 1
        return left


276
你是产品经理,目前正在带领一个团队开发新的产品。不幸的是,你的产品的最新版本没有通过质量检测。由于每个版本都是基于之前的版本开发的,所以错误的版本之后的所有版本都是错的。

假设你有 n 个版本 [1, 2, …, n],你想找出导致之后所有版本出错的第一个错误的版本。

你可以通过调用bool isBadVersion(version)接口来判断版本号 version 是否在单元测试中出错。
实现一个函数来查找第一个错误的版本。你应该尽量减少对调用 API 的次数。

示例:
给定 n = 5,并且 version = 4 是第一个错误的版本。

调用 isBadVersion(3) -> false
调用 isBadVersion(5)-> true
调用 isBadVersion(4)-> true

所以,4 是第一个错误的版本。

# 二分
# 将左右边界分别初始化为 1 和 n,其中 n 是给定的版本数量。
# 设定左右边界之后,每次我们都依据左右边界找到其中间的版本,检查其是否为正确版本。
# 如果该版本为正确版本,那么第一个错误的版本必然位于该版本的右侧,我们缩紧左边界;
# 否则第一个错误的版本必然位于该版本及该版本的左侧,我们缩紧右边界。
# 这样我们每判断一次都可以缩紧一次边界,而每次缩紧时两边界距离将变为原来的一半,因此我们至多只需要缩紧 O(\log n)O(logn) 次。
class Solution:
    def firstBadVersion(self, n):
        left, right = 1, n

        while left < right:
            mid = (left + right) // 2
            if isBadVersion(mid):
                right = mid
            else:
                left = mid + 1

        return left

注意:
应该避免使用 mid = (left + right) // 2;
因为这样有些时候会出现数字过大的溢出情况。
所以, 应该使用 mid = left + (right - left) / 2;


852
符合下列属性的数组 arr 称为 山脉数组 :
arr.length >= 3
存在 i(0 < i < arr.length - 1)使得:
arr[0] < arr[1] < … arr[i-1] < arr[i]
arr[i] > arr[i+1] > … > arr[arr.length - 1]
给你由整数组成的山脉数组 arr ,返回任何满足 arr[0] < arr[1] < … arr[i - 1] < arr[i] > arr[i + 1] > … > arr[arr.length - 1] 的下标 i 。

示例 1:
输入:arr = [0,1,0]
输出:1

示例 2:
输入:arr = [0,2,1,0]
输出:1

示例 3:
输入:arr = [0,10,5,2]
输出:1

# 暴力
class Solution:
    def peakIndexInMountainArray(self, arr: List[int]) -> int:
        for i in range(1, len(arr) - 1):
            if arr[i] > arr[i + 1]:
                return i

# 二分
class Solution:
    def peakIndexInMountainArray(self, arr: List[int]) -> int:
        n = len(arr)
        left, right, res = 1, n - 1, 0

        while left < right:
            mid = (left + right) // 2
            if arr[mid] > arr[mid + 1]:  # [mid, right] 为上升区间
                res = mid
                right = mid
            else:  # [left, mid] 为上升区间
                left = mid + 1

        return res


528
给定一个正整数数组w ,其中w[i]代表下标 i的权重(下标从 0 开始),请写一个函数pickIndex,它可以随机地获取下标 i,选取下标 i的概率与w[i]成正比。
例如,对于 w = [1, 3],挑选下标 0 的概率为 1 / (1 + 3)= 0.25 (即,25%),而选取下标 1 的概率为 3 / (1 + 3)= 0.75(即,75%)。
也就是说,选取下标 i 的概率为 w[i] / sum(w) 。

示例 1:
输入:
[“Solution”,“pickIndex”]
[[[1]],[]]
输出:
[null,0]
解释:
Solution solution = new Solution([1]);
solution.pickIndex(); // 返回 0,因为数组中只有一个元素,所以唯一的选择是返回下标 0。

示例 2:
输入:
[“Solution”,“pickIndex”,“pickIndex”,“pickIndex”,“pickIndex”,“pickIndex”]
[[[1,3]],[],[],[],[],[]]
输出:
[null,1,1,1,1,0]
解释:
Solution solution = new Solution([1, 3]);
solution.pickIndex(); // 返回 1,返回下标 1,返回该下标概率为 3/4 。
solution.pickIndex(); // 返回 1
solution.pickIndex(); // 返回 1
solution.pickIndex(); // 返回 1
solution.pickIndex(); // 返回 0,返回下标 0,返回该下标概率为 1/4 。

由于这是一个随机问题,允许多个答案,因此下列输出都可以被认为是正确的:
[null,1,1,1,1,0]
[null,1,1,1,1,1]
[null,1,1,1,0,0]
[null,1,1,1,0,1]
[null,1,0,1,0,0]

诸若此类。

class Solution:
    def __init__(self, w: List[int]):
        self.pre = list(itertools.accumulate(w))  # 进行前缀和计算
        self.total = self.pre[-1]  # 和

    def pickIndex(self) -> int:
        x = random.randint(1, self.total)
        return bisect_left(self.pre, x)  # 返回索引

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值