二分查找詳解【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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章