二分查找算法 四种题型六道题目总结,从此二分不迷路!

前言

二分查找在算法中一般有四类题目:

  1. 排序或通过排序后的数组,快速求某个值的下标
  • 35.搜索插入位置
  1. 求某个值在数组中的左右端点
  • 34.在排序数组中查找元素的第一个和最后一个位置 (中等)
  1. 通过条件判断进行二分查找
  • 278.第一个错误的版本
  • 875.爱吃香蕉的珂珂(中等)
  1. 局部有序的二分查找
  • 33.搜索旋转排序数组(中等)
  • 81.搜索旋转排序数组II(中等)

今天将这四类列举六道题,让大家一次看个够,从此二分不迷路!

35.搜索插入位置

https://leetcode-cn.com/problems/search-insert-position/solution/35sou-suo-cha-ru-wei-zhi-pythonji-chu-er-c0xq/

难度:简单

题目

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

你可以假设数组中无重复元素。

示例

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

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

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

分析

手写二分解题

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

api解题

class Solution:
    def searchInsert(self, nums, target):
        return bisect.bisect_left(nums, target)

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

https://leetcode-cn.com/problems/find-first-and-last-position-of-element-in-sorted-array/solution/34zai-pai-xu-shu-zu-zhong-cha-zhao-yuan-zoga7/

难度:中等

题目

给定一个按照升序排列的整数数组 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]

分析

二分查找左右端点有一个小tips

  1. left = bisect_left(target),如果left== length or nums[left] != target,
    表示没有找到该数字,返回[-1, -1]
  2. 在基于left的前提下,我们bisect_left(target + 1),
    即可获取target下一个数字的插入点,然后right -1就是结果了。

解题

import bisect
class Solution:
    def searchRange(self, nums, target):
        left = bisect.bisect_left(nums,target)
        if left == len(nums) or nums[left] != target:
            return [-1,-1]
        right = bisect.bisect_left(nums,target+1)
        return [left,right - 1]

278.第一个错误的版本

https://leetcode-cn.com/problems/first-bad-version/solution/278di-yi-ge-cuo-wu-de-ban-ben-by-qingfen-gp99/

难度:简单

题目:

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

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

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

示例:

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

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

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

分析

这是一道标准的,读题3分钟解题30秒题目。
其实只需要关注查找、尽可能少的次数这些关键字就可以判断出是一道二分查找的题目了。
题目中虚拟构建了 isBadVersion 方法用于判断结果是True or False。

所以,今天只能说是力扣庆祝端午快乐的一道放水送分题...

解题:

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

875.爱吃香蕉的珂珂

https://leetcode-cn.com/problems/koko-eating-bananas/solution/875-ai-chi-xiang-jiao-de-ke-ke-er-fen-ch-7you/

难度:中等

题目:

珂珂喜欢吃香蕉。这里有N堆香蕉,第 i 堆中有piles[i]根香蕉。警卫已经离开了,将在H小时后回来。

珂珂可以决定她吃香蕉的速度K(单位:根/小时)。每个小时,她将会选择一堆香蕉,从中吃掉 K 根。如果这堆香蕉少于 K 根,她将吃掉这堆的所有香蕉,然后这一小时内不会再吃更多的香蕉。

珂珂喜欢慢慢吃,但仍然想在警卫回来前吃掉所有的香蕉。

返回她可以在 H 小时内吃掉所有香蕉的最小速度 K(K 为整数)。

示例:

示例 1:

输入: piles = [3,6,7,11], H = 8

输出: 4

示例2:

输入: piles = [30,11,23,4,20], H = 5

输出: 30

示例3:

输入: piles = [30,11,23,4,20], H = 6

输出: 23

分析:

由于1 <= piles.length <= 10^4, piles.length <= H <= 10^9所以肯定是二分查找没跑了。

下来看到最小时间,肯定是二分查找左边界。至于如何选择最大值,题目中描述:

“如果香蕉小于K根,他讲吃掉这堆的所有,并一小时内不会再吃更多香蕉”

所以我们最大值选择这个列表的max值即可。

解题:

class Solution:
    def minEatingSpeed(self, piles, h):
        def cost(k):
            t = 0
            for i in piles:
                t += (i + k - 1) // k
            return t

        left, right = 1, max(piles)
        while left < right:
            mid = (left + right) // 2
            ret = cost(mid)
            if ret <= h:
                right = mid
            else:
                left = mid + 1
        return left

33.搜索旋转排序数组

https://leetcode-cn.com/problems/search-in-rotated-sorted-array/solution/33sou-suo-xuan-zhuan-pai-xu-shu-zu-pytho-2oia/

难度:中等

题目

整数数组 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.length <= 5000
  • -10^4 <= nums[i] <= 10^4
  • nums 中的每个值都 独一无二
  • 题目数据保证 nums 在预先未知的某个下标上进行了旋转
  • -10^4 <= target <= 10^4

进阶:你可以设计一个时间复杂度为 O(log n) 的解决方案吗?

示例

示例 1:

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

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

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

分析

第一次看这个题的时候,这能算中等?循环判断在不在不就完了。暴力通过,咦,效率还挺高?
沾沾自喜中看到了进阶要求,使用时间复杂度为O(log n)的方法完成...
O(log n)无疑二分才能实现,但是数组时无序的啊,我们先排序再二分?别逗,排序的时间复杂度就是O(n).
那么如何对局部有序的数组进行二分搜索呢?让我们以示例1为例:
nums = [4,5,6,7,0,1,2], target = 0
设置左右端点,left = 0 right = len(nums) -1。
有的小伙伴们要问了,你求length不是要一个一个加起来,复杂度不是O(n)么,错!
一定要记得Python的len()方法在获取数组长度时的时间复杂度是O(1).至于为啥篇幅原因百度吧...
mid = (0 + 6) // 2 == 3,此时nums[mid] == 7,下面开始判断。

  1. nums[mid]是否等于target,等于返回mid
  2. 关键来了,nums[mid]是否大于nums[left]
    • 如果nums[left] <= nums[mid]
      • nums[left] <= target < nums[mid],此时我们缩减right = mid - 1
      • 否则,缩减left = mid + 1
    • 如果nums[left] > nums[mid]
      • nums[mid] < target <= nums[right],此时我们缩减left += 1
      • 否则,缩减right = mid - 1
        我们通过将局部有序的数据进行分场景考虑的情况,完成了二分的实现,这就是局部有序数组的二分操作方式。

暴力解法

class Solution:
    def search(self, nums, target):
        for i, num in enumerate(nums):
            if target == num:
                return i
        return -1

解题

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

81.搜索旋转排序数组II

https://leetcode-cn.com/problems/search-in-rotated-sorted-array-ii/solution/81sou-suo-xuan-zhuan-pai-xu-shu-zu-ii-33-7lz5/

难度:中等

题目

已知存在一个按非降序排列的整数数组 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.length <= 5000
  • -10^4 <= nums[i] <= 10^4
  • 题目数据保证 nums 在预先未知的某个下标上进行了旋转
  • -10^4 <= target <= 10^4

进阶:

  • 这是 搜索旋转排序数组 的延伸题目,本题中的 nums 可能包含重复元素。
  • 这会影响到程序的时间复杂度吗?会有怎样的影响,为什么?

示例

示例 1:

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

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

分析

先回答进阶问题,nums 可能包含重复元素,这会影响到程序的时间复杂度吗?
两种答案:

  1. 不会,我就是暴力return target in nums 管你有没有重复值呢,哈哈。
  2. 会,使用二分查找局部有序时,当nums[mid] == nums[left]时,
    我无法知道他到底在翻转前的数组还是反转后的数组。只能left左移一位继续判断。

好了,做这道题之前,强烈建议先看看它的基础版题目,然后再来做这道题:

  • 33.搜索旋转排序数组

其实这道题,我们只需要根据33题的搜索改变一点判断就行,即当nums[left] == nums[mid] == nums[right],
此时我们不知道该怎么移动,那就left += 1 right += 1,再去判断即可。

解题

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

欢迎关注我的公众号: 清风Python,带你每日学习Python算法刷题的同时,了解更多python小知识。

我的个人博客:https://qingfengpython.cn

力扣解题合集:https://github.com/BreezePython/AlgorithmMarkdown

你可能感兴趣的:(二分查找算法 四种题型六道题目总结,从此二分不迷路!)