[算法学习04] 二分查找系列

前言

本期包括闭区间、左闭右开、左开右开三个区间上的二分查找写法,循环不变量的理解以及查找≥、>、<、≤等内容。

1.闭区间上的二分查找

前提条件,数组排列有序。

核心定义,循环不变量。 根据区间内的元素与target的关系都是未知的,区间外的元素与target的关系都是已知的。

得到定义 left的左边均小于 target,right的右边都是大于等于target的。这句话的代码为

if nums[mid] < target:
    left = mid + 1
else:
    right = mid - 1



# 如果写成下面这样,就是left的左边都是小于等于target的元素, right的右边都是大于target的元素

if nums[mid] > target:
    left = mid + 1
else:
    right = mid - 1

if 条件会将 大于等于 和 小于 进行分组

下面给出闭区间二分查找的代码。

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

2.左闭右开区间上的二分查找

[left,right)

根据区间内的元素与target的关系都是未知的,区间外的元素与target的关系都是已知的。

得到定义 left的左边均小于 target,right-1的右边都是大于等于target的。故当循环结束时,left和right都是我们要找的答案!

class Solution:
    def search(self, nums: List[int], target: int) -> int:
        # [left, right)
        left, right = 0, len(nums)
        # [1,1) 试想一下,这样的区间存在吗? 所以left和right相等循环结束
        while left < right:
            mid = (left + right) // 2
            if nums[mid] < target:
                left = mid + 1
            else:
                right = mid
        return left if left != len(nums) and nums[left] == target else -1

3. 左开右开区间上的二分查找

(left,right)

根据区间内的元素与target的关系都是未知的,区间外的元素与target的关系都是已知的。

得到定义 left+1的左边均小于 target,right-1的右边都是大于等于target的。故当循环结束时,left+1和right都是我们要找的答案!

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

4. leetcode 34. 在排序数组中查找元素的第一个和最后一个位置

给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。

如果数组中不存在目标值 target,返回 [-1, -1]

你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。

示例 1:

输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]

示例 2:

输入:nums = [5,7,7,8,8,10], target = 6
输出:[-1,-1]

示例 3:

输入:nums = [], target = 0
输出:[-1,-1]

提示:

  • 0 <= nums.length <= 105
  • -109 <= nums[i] <= 109
  • nums 是一个非递减数组
  • -109 <= target <= 109

解题思路:

在了解了上面的内容以后,这道题就显得非常容易了。 随便选择二分查找1-3的一种写法,然后调用函数,我们就得到了第一次出现的位置。接下来是值得注意的地方,如果返回值start == len(nums),说明数组中不存在>=target的数,如果nums[start] != target 则说明 数组中不存在target。 以上两种情况,均需要返回[-1, -1]。

如果 start存在,接下来 我们要找的就是最后一次出现的位置, 那么只要找到target + 1第一次出现的位置然后 -1,就得到了 target最后出现的位置。

以下是代码

def lower_bound(nums, target):
    left, right = 0, len(nums)-1
    while left <= right:
        mid = (left + right) // 2
        if nums[mid] < target:
            left = mid + 1
        else:
            right = mid - 1
    return left

class Solution:
    def searchRange(self, nums: List[int], target: int) -> List[int]:
        start = lower_bound(nums, target)
        if start == len(nums) or nums[start] != target:
            return [-1, -1]
        end = lower_bound(nums, target+1) - 1
        return [start, end] 

5. leetcode 35. 搜索插入位置 https://leetcode.cn/problems/search-insert-position/

给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。

请必须使用时间复杂度为 O(log n) 的算法。

示例 1:

输入: nums = [1,3,5,6], target = 5
输出: 2

示例 2:

输入: nums = [1,3,5,6], target = 2
输出: 1

示例 3:

输入: nums = [1,3,5,6], target = 7
输出: 4

提示:

  • 1 <= nums.length <= 104
  • -104 <= nums[i] <= 104
  • nums 为 无重复元素 的 升序 排列数组
  • -104 <= target <= 104

解题思路:

这题非常简单,要找到target的插入位置,就是说找到第一个>=target的位置,直接调用二分查找的函数即可。以下是代码

def lower_bound(nums, target):
    left, right = 0, len(nums)-1
    while left <= right:
        mid = (left + right) // 2
        if nums[mid] < target:
            left = mid + 1
        else:
            right = mid - 1
    return left

class Solution:
    def searchInsert(self, nums: List[int], target: int) -> int:
        return lower_bound(nums, target)

6. leetcode 704. 二分查找 https://leetcode.cn/problems/binary-search/

解题思路:直接调用函数即可,需要注意的是如果找不到的话需要返回-1(我这里使用的开区间的写法,如果right >= len(nums) 或者 nums[right] != target 说明nums中不存在target)。

以下是代码

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

7. leetcode 2529. 正整数和负整数的最大计数 https://leetcode.cn/problems/maximum-count-of-positive-integer-and-negative-integer/

给你一个按 非递减顺序 排列的数组 nums ,返回正整数数目和负整数数目中的最大值。

  • 换句话讲,如果 nums 中正整数的数目是 pos ,而负整数的数目是 neg ,返回 pos 和 neg二者中的最大值。

注意:0 既不是正整数也不是负整数。

示例 1:

输入:nums = [-2,-1,-1,1,2,3]
输出:3
解释:共有 3 个正整数和 3 个负整数。计数得到的最大值是 3 。

示例 2:

输入:nums = [-3,-2,-1,0,0,1,2]
输出:3
解释:共有 2 个正整数和 3 个负整数。计数得到的最大值是 3 。

示例 3:

输入:nums = [5,20,66,1314]
输出:4
解释:共有 4 个正整数和 0 个负整数。计数得到的最大值是 4 。

解题思路:

这道题也很简单,需要注意的就是0既不是正整数也不是负整数。

首先找到第一个 >= 1的位置 就是 第一个正整数的位置 记作positive

然后找到 第一个>= 0的位置 -1 就是 第一个负整数的位置 记作negative

len(nums[positive: ]) 正整数的个数

len(nums[0: negative+1]) 负整数的个数

以下是代码

def lower_bound(nums, target):
    left, right = 0, len(nums)-1
    while left <= right:
        mid = (left + right) // 2
        if nums[mid] < target:
            left = mid + 1
        else:
            right = mid - 1
    return left
class Solution:
    def maximumCount(self, nums: List[int]) -> int:
        positive = lower_bound(nums, 1)
        negative = lower_bound(nums, 0) - 1
        p_n = len(nums[positive:])
        n_n = len(nums[0:negative+1])
        return max(p_n, n_n)

8. leetcode 2300. 咒语和药水的成功对数 https://leetcode.cn/problems/successful-pairs-of-spells-and-potions/

给你两个正整数数组 spells 和 potions ,长度分别为 n 和 m ,其中 spells[i] 表示第 i 个咒语的能量强度,potions[j] 表示第 j 瓶药水的能量强度。

同时给你一个整数 success 。一个咒语和药水的能量强度 相乘 如果 大于等于 success ,那么它们视为一对 成功 的组合。

请你返回一个长度为 n 的整数数组 pairs,其中 pairs[i] 是能跟第 i 个咒语成功组合的 药水 数目。

示例 1:

输入:spells = [5,1,3], potions = [1,2,3,4,5], success = 7
输出:[4,0,3]
解释:
- 第 0 个咒语:5 * [1,2,3,4,5] = [5,10,15,20,25] 。总共 4 个成功组合。
- 第 1 个咒语:1 * [1,2,3,4,5] = [1,2,3,4,5] 。总共 0 个成功组合。
- 第 2 个咒语:3 * [1,2,3,4,5] = [3,6,9,12,15] 。总共 3 个成功组合。
所以返回 [4,0,3] 。

示例 2:

输入:spells = [3,1,2], potions = [8,5,8], success = 16
输出:[2,0,2]
解释:
- 第 0 个咒语:3 * [8,5,8] = [24,15,24] 。总共 2 个成功组合。
- 第 1 个咒语:1 * [8,5,8] = [8,5,8] 。总共 0 个成功组合。
- 第 2 个咒语:2 * [8,5,8] = [16,10,16] 。总共 2 个成功组合。
所以返回 [2,0,2] 。

提示:

  • n == spells.length
  • m == potions.length
  • 1 <= n, m <= 105
  • 1 <= spells[i], potions[i] <= 105
  • 1 <= success <= 1010

解题思路:

这题思路不难,但是想过leetcode的样例超级难! 

实际上的思路就是 potions这个数组排个序,然后用spell也就是咒语乘过去 记作search,接着用二分查找找到乘完后数组中第一个大于等于target的位置 记作X,然后len(search[x:])。 这样就是基本思路,然并卵,超出时间限制在这折磨你

看了别人的思路,将spell * potions 改成了 success / spell ,这个时候还是不行,仍然超出时间限制!

又看看了别人的思路,改成了len(potions) - x,这个时候过了样例。

以下是代码

def lower_bound(nums, target):
    left, right = 0, len(nums)
    while left < right:
        mid = (left + right) // 2
        if nums[mid] < target:
            left = mid + 1
        else:
            right = mid
    return left
class Solution:
    def successfulPairs(self, spells: List[int], potions: List[int], success: int) -> List[int]:
        ans = []
        potions.sort()
        for spell in spells:
            # search = [spell *p for p in potions] 
            x = lower_bound(potions, success / spell)
            ans.append(len(potions) - x)
        return ans 

解释如下(感谢万能的GPT):

这两个方法之所以在性能上有所不同,是因为它们在查找满足条件的配对数时使用了不同的逻辑。让我们来看一下每个方法的行为:

  1. ans.append(len(potions[x:]))

    • 这种方法使用二分查找来找到第一个大于等于 success / spell 的元素的索引 x
    • 然后它计算从索引 x 到列表末尾的元素数量,以确定满足条件的配对数。
    • 问题在于,potions[x:] 创建了一个新的列表切片,这需要额外的时间和空间复杂度来创建和遍历切片,尤其当 potions 列表很大时,这会导致超出时间限制。
  2. ans.append(len(potions) - x)

    • 这种方法也使用了二分查找来找到第一个大于等于 success / spell 的元素的索引 x
    • 然后它计算满足条件的配对数,直接使用 len(potions) - x
    • 这种方法没有创建新的列表切片,因此没有额外的时间和空间开销,因此性能更好。

综上所述,第二种方法在性能上更优,因为它避免了创建额外的列表切片。

9. Leetcode 275. H 指数 II https://leetcode.cn/problems/h-index-ii/solution/tu-jie-yi-tu-zhang-wo-er-fen-da-an-si-ch-d15k/

给你一个整数数组 citations ,其中 citations[i] 表示研究者的第 i 篇论文被引用的次数,citations 已经按照 升序排列 。计算并返回该研究者的 h 指数。

h 指数的定义:h 代表“高引用次数”(high citations),一名科研人员的 h 指数是指他(她)的 (n 篇论文中)至少 有 h 篇论文分别被引用了至少 h 次。

请你设计并实现对数时间复杂度的算法解决此问题。

示例 1:

输入:citations = [0,1,3,5,6]
输出:3
解释:给定数组表示研究者总共有 5 篇论文,每篇论文相应的被引用了 0, 1, 3, 5, 6 次。由于研究者有3 篇论文每篇 至少 被引用了 3 次,其余两篇论文每篇被引用 不多于 3 次,所以她的 h 指数是 3。

示例 2:

输入:citations = [1,2,100]
输出:2

提示:

  • n == citations.length
  • 1 <= n <= 105
  • 0 <= citations[i] <= 1000
  • citations 按 升序排列

解题思路:

这道题好难。

首先理解一下什么是H指数?  就是说 某位作者的 k篇论文的引用数 都大于K ,那么他的H指数就是k。

例如 A作者发表六篇论文[0,1,2,3,5,6],列表的数字是他每篇论文的引用数。  可以看出他有三篇论文的引用数都>3,所以他的H指数就是3。

这道题的思路是二分答案,如果一位作者有 3篇论文的引用数 > 3, 那么他也肯定有 2篇论文的引用数 >2。 如果一位作者没有 4篇论文的引用数 都 > 4, 那么他肯定也不存在 5篇论文都 > 5。 

以下是代码

class Solution:
    def hIndex(self, citations: List[int]) -> int:
        # 论文的数量是[1,n]
        left, right = 1, len(citations)
        while left <= right:
            mid = (left + right) // 2

            # 如果 mid篇论文的引用数 都大于 mid, 那么 mid-1篇论文的引用数都大于mid-1,所以更新左边界
            if citations[-mid] >= mid:
                left = mid + 1
            # 如果没有mid篇论文的引用 都大于mid, 那么 mid+1篇论文的引用数肯定小于 mid+1,所以更新右边界
            else:
                right = mid - 1

        # 根据循环不变量 可知left - 1为 存在h篇论文的引用数>h
        # right + 1 为不存在 h篇论文的引用数>h
        # 所以返回的答案为 right or left - 1
        return right 

之前我在想一个问题,就是为什么要返回right? 

现在相同了,因为left每次更新为mid + 1 而 right 每次更新为 mid - 1

left的更新前提是 存在 mid篇文章的引用指数 >= mid,更新为 mid  + 1

right的更新前提是 不存在mid篇文章的引用指数 >= mid, 更新为 mid - 1

当循环结束时 left > right ,为什么返回right而不是left,这是因为在最后一次循环中,left指针会移动到mid + 1,而right指针会保持在mid - 1。由于left指针移动到了不满足条件的区域,而right指针停留在满足条件的区域,因此我们应该返回right作为答案。

总结

二分查找的思路很简单,但需要把握住循环不变量的核心!还有一句很重要的话,区间内的元素关系位置,区间外的元素关系已知。

  • 17
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值