二分查找详解【Python】

在此记录下二分查找的常用模板,包括查找指定数、查找左边界和右边界,以后解题就用这个模板。

一、查找指定数(基本的二分搜索)

def binarySearch(nums, target):
  left, right = 0, len(nums)-1  # 搜索区间两边为闭
  while left <= right:  # 注意停止条件,停止条件为[left, left+1]
    mid = left + (right - left) // 2
    # 所有情况都写出来
    if nums[mid] == target:
      return mid
    elif nums[mid] < target:
      left = mid + 1  # 因为mid已经搜索过
		else:
      right = mid - 1
   return -1

注意点

  1. 关于while left <= right为什么要取等号,是取决于leftright的初始值,或者说取决于搜索区间是开还是闭的问题。比如,如果left, right = 0, len(nums) - 1,那么说明搜索区间是两端都闭区间,因此循环的停止条件就应该是搜索区间为空,即[left, left + 1]。如果不加等号,那么到[left, left]就停止了,此时left没有被查找过,不正确。我们这里和下面采用的是两端都闭的搜索区间。
  2. 关于为什么 left = mid + 1,right = mid - 1,这也是跟搜索区间有关,因为我们搜索区间两端都闭,所以当mid已经被查找过,那么下一次当然是mid + 1 和 mid - 1了。

二、查找左边界,即第一个等于目标数的位置

def left_bound(nums, target):
	"""
	[1, 2, 4, 4, 5], target = 4
	"""
    left, right = 0, len(nums) - 1
    while left <= right:
        mid = left + (right - left) // 2
        if nums[mid] < target:
            left = mid + 1
        elif nums[mid] > target:
            right = mid - 1
        else:
            right = mid - 1
    # 最终停止时right在2位置,left在4位置,所以返回left
    if left >= len(nums) or nums[left] != target:
        return -1
    return left
  1. 采用两端闭区间作为搜索区间
  2. 等于情况的更新,因为是左边界,所以更新右端点,right = mid - 1
  3. 异常情况判断:当停止时,left超过索引或nums[left] != target
  4. 最终情况是right在1的位置,left在2的位置,所以返回left

三、查找右边界,即最后一个等于目标数的位置

def right_bound(nums, target):
	"""
	[1, 2, 4, 4, 5], target = 4
	"""
    left, right = 0, len(nums) - 1
    while left <= right:
        mid = left + (right - left) // 2
        if nums[mid] < target:
            left = mid + 1
        elif nums[mid] > target:
            right = mid - 1
        else:
            left = mid + 1
        #  最终停止时left在5位置,right在4位置,所以返回left-1
    if left >= len(nums) or nums[left - 1] != target:
        return -1
    return left - 1

四、其他情况

  1. 查找第一个大于等于目标值的位置,[1, 2, 4, 4, 6], target = 3
    答:相当于查左边界,把最后的判断条件nums[left] != target删掉即可
  2. 查找最后一个小于等于目标值的位置,[1, 2, 4, 4, 6], target = 5
    答:相当于查右边界,把最后的判断条件nums[left - 1] != target删掉即可

五、二分查找具体应用

首先我们要知道,二分查找只适用于有序数组,那么除了上面我们讲的在有序数组中查找目标值以及边界,抛开有序数组这个枯燥的数据结构,二分查找如何运用到实际的算法问题中呢?当搜索空间有序的时候,就可以通过二分搜索「剪枝」,大幅提升效率。
下面用几个例题来说明二分查找在搜索空间中的优化求解

875. 爱吃香蕉的珂珂

珂珂喜欢吃香蕉。这里有 N 堆香蕉,第 i 堆中有 piles[i] 根香蕉。警卫已经离开了,将在 H 小时后回来。珂珂可以决定她吃香蕉的速度 K (单位:根/小时)。每个小时,她将会选择一堆香蕉,从中吃掉 K 根。如果这堆香蕉少于 K 根,她将吃掉这堆的所有香蕉,然后这一小时内不会再吃更多的香蕉。
珂珂喜欢慢慢吃,但仍然想在警卫回来前吃掉所有的香蕉。
返回她可以在 H 小时内吃掉所有香蕉的最小速度 K(K 为整数)

先抛开二分查找技巧,想想如何暴力解决这个问题呢?
首先,算法要求的是「H 小时内吃完香蕉的最小速度」,我们不妨称为 speed,请问 speed最大可能为多少,最少可能为多少呢?
显然最少为1,最大为max(piles),因为一小时最多只能吃一堆香蕉。那么暴力解法就很简单了,只要从 1开始穷举到 max(piles),一旦发现发现某个值可以在 H 小时内吃完所有香蕉,这个值就是最小速度。

那么二分查找如何优化呢?
由于我们要求的是最小速度,所以其实就是在搜索范围[1, max(piles)]内找到满足条件的左边界。我们定义一个函数isValid作为二分查找更新的判断条件,由于我们求左边界,所以当满足的时候更新右端点即可。

class Solution:
    def minEatingSpeed(self, piles, H):
        # 1. 首先可以缩小K的范围, 最小是1,最大是pile里面最大的那一堆
        maxnn = -1
        for i in range(len(piles)):
            if piles[i] > maxnn:
                maxnn = piles[i]

        def isValid(speed):
            # import math
            time = 0
            for j in range(len(piles)):
                tmp = piles[j] % speed
                if tmp == 0:
                    this_time = piles[j] // speed
                else:
                    this_time = piles[j] // speed + 1
                time += this_time
            return time <= H
        # 2. 二分查找[1, maxnn]里满足isValid的最小值
        left, right = 1, maxnn
        while left <= right:
            mid = left + (right - left) // 2
            if isValid(mid):  # 满足的话,缩小右边界
                right = mid - 1
            else:
                left = mid + 1

        return left

1011. 在 D 天内送达包裹的能力

传送带上的包裹必须在 D 天内从一个港口运送到另一个港口。
传送带上的第 i 个包裹的重量为 weights[i]。每一天,我们都会按给出重量的顺序往传送带上装载包裹。我们装载的重量不会超过船的最大运载重量。
返回能在 D 天内将传送带上的所有包裹送达的船的最低运载能力。
输入:weights = [1,2,3,4,5,6,7,8,9,10], D = 5
输出:15
解释:
船舶最低载重 15 就能够在 5 天内送达所有包裹,如下所示:
第 1 天:1, 2, 3, 4, 5
第 2 天:6, 7
第 3 天:8
第 4 天:9
第 5 天:10
请注意,货物必须按照给定的顺序装运,因此使用载重能力为 14 的船舶并将包装分成 (2, 3, 4, 5), (1, 6, 7), (8), (9), (10) 是不允许的。

本质上和 Koko 吃香蕉的问题一样的,首先确定 最小值和最大值分别为 max(weights)sum(weights)。然后在该区间内寻找左边界即可。

class Solution:
    def shipWithinDays(self, weights, D):
        # 1.首先确定运输能力的上下界,分别为所有货物的和,所有货物中最重的那个
        left, right = max(weights), sum(weights)

        # 2. 二分查找
        def isValid(p):  # 以p能力能否在D天内运送好
            days = 0
            i = 0
            cur_sum = 0  # 当前货物的总重量
            while i < len(weights):
                cur_sum += weights[i]
                if cur_sum > p:
                    days += 1
                    cur_sum = weights[i]
                i += 1
            if cur_sum != 0:
                days += 1  # 最后一波
            return days <= D

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

        return left

所以,经过上面两个例子,我们发现:对于寻找有序区间内的最优解,我们可以不用穷举暴力搜索的方式,而是可以转化成为二分查找左边界或右边界的问题,对于这类问题,使用二分查找的思路和模板可以写成以下形式:

def findOptim():
	# 1. 首先求出解的范围
	min_value, max_value = xx, xx  
	def isValid():
		"""
		表示满足条件的函数
		"""
	# 2. 区间内使用二分查找
	left, right = min_value, max_value
	while left <= right:
		mid = left + (right - left) // 2
		if isValid(mid):
		else:
	

二分查找高效判定子序列
如何判定字符串 s 是否是字符串 t 的子序列(可以假定 s 长度比较小,且 t 的长度非常大)。

s = “abc”, t = “ahbgdc”, return true.
s = “axc”, t = “ahbgdc”, return false.

题目很简单,也很难想到和二分查找有什么关联。首先,通常的双指针解法是这样的

def isSubsequence(s, t):
	i, j = 0, 0
	while i < len(s) and j < len(t):
		if s[i] == t[j]:
			i += 1
			j += 1
		else:
			j += 1
	return i == len(s)

这个解法的时间复杂度是O(n)O(n)nn为字符串t的长度。如果仅仅是这个问题,那么这种解法是最优解。
但是如果给你一系列字符串 s1,s2,...和字符串t,你需要判定每个串s是否是t的子序列(可以假定 s较短,t很长)。如果再用上面的解法,那么就是对于每一个s,都按照遍历一遍t的方法操作一遍,时间复杂度是$O(mn)。可是当t串的长度非常大时,时间复杂度就很高了。那么如何使用二分查找,使得复杂度大大降低呢?
二分查找思路:
对串t进行预处理,遍历一遍,将每个字符的下标存放在map中,<key, value> = <s, index>
在这里插入图片描述
那么有了这个map之后,就可以使用二分查找了。举例来说,当s=abc,已经匹配了ab,只需要去map[c]中查找第一个比index=3大的下标即可。因此,对于s中的每一个字符,只需要去map中用二分查找到对应的左边界index,其中左边界的target值是上一个字符匹配到在t中的index

这样的话, 算法复杂度为O(mlogn)O(mlogn)。在nn很大,而mm相对较小的时候可以大大降低复杂度

def isSubsequence(s, t):
    # 预处理,构建字符和下标的字典
    map = {}  # 字典
    for i in range(len(t)):
        if t[i] in map.keys():
            map[t[i]].append(i)
        else:
            map[t[i]] = [i]

    # 查找第一个比target大的数
    def left_bound(nums, target):
        left, right = 0, len(nums) - 1
        while left <= right:
            mid = left + (right - left) // 2
            if nums[mid] <= target:
                left = mid + 1
            elif nums[mid] > target:
                right = mid - 1
        return left

    j = 0  # t中的开始位置
    for i in range(len(s)):
        # t中没有s[i]
        if s[i] not in map.keys():
            return False
        pos = left_bound(map[s[i]], j)
        # s[i]在t中的index没有比j还大的
        if pos == len(map[s[i]]):
            return False
        j = pos
    return True
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章