二分查找及對應的幾道經典題目

二分查找(Binary Search)屬於七大查找算法之一,又稱折半查找,它的名字很好的體現出了它的基本思想,二分查找主要是針對的是有序存儲的數據集合。

假設有一個集合和一個待查找的目標值,每次都通過將目標值和處於集合中間位置的元素比較,將待查找區間收縮爲之前區間的一半,比如目標值小於一次二分查找區間的中間值,則下次查找區間就爲原區間的左邊一半,重複此過程直至找到目標值或者區間被收縮爲0.

下面這幅動圖就爲二分查找的基本過程,也是最簡單的一種二分查找。

最開始我們總是維護兩個指針,分別指向數組的起始位置和終止位置,這樣做是方便找出數組中間位置,然後下次二分查找時,根據比較結果,選擇移動left指針還是right指針來縮小數組長度,二分查找的一個最基本框架如下:

def BinarySearch(nums,target):
    left,right = 0,len(nums)-1
    while left<=right:
        mid = left+(right-left)//2 #計算數組中間位置
        if nums[mid] == target:
            return mid #找到並返回
        # 中間元素大於目標值,搜索數組左半部分
        elif nums[mid] > target:
            right = mid - 1
        # 中間元素小於目標值,搜索數組右半部分
        elif nums[mid] < target:
            left = mid + 1
    return -1 #沒找到目標值

如果假設數組的長度爲n,每次二分查找後數組長度都會縮小至之前的一半,很容易得出二分查找的時間複雜度爲O(log2n)O(log_2n),沒有利用額外的存儲空間,所以空間複雜度爲O(1)O(1)

下面通過LeetCode上三道明顯體現二分查找思想的例題進一步深入瞭解此算法的思想,這四道題都是以上面框架爲基礎,只是在題目中添加了一些約束條件,都爲medium難度,會給出對應的題號。

來源:力扣(LeetCode)
鏈接:https://leetcode-cn.com

33. 搜索旋轉排序數組

假設按照升序排序的數組在預先未知的某個點上進行了旋轉。( 例如,數組 [0,1,2,4,5,6,7] 可能變爲 [4,5,6,7,0,1,2] )。搜索一個給定的目標值,如果數組中存在這個目標值,則返回它的索引,否則返回 -1 。

注意:

  • 你可以假設數組中不存在重複的元素。
  • 你的算法時間複雜度必須是 O(log n) 級別。


這道題的注意中已經明確要求了時間複雜度,這幾乎是指定了這道題目要求你用二分查找實現。可是上面說過二分查找是針對有序數組的查找算法,這裏就有些矛盾了,但是利用二分查找是完全可行的。

首先需要明確的一個事實是這個數組並不是完全無序,它只是在某個點進行旋轉,那麼以這個點分隔開的兩部分一定是有序的,那它就滿足二分查找的條件。在知道這個事實之後,我們可以通過mid指針將數組分爲左右兩部分,這兩部分其中之一必然有序,然後就可以判斷出目標值是否存在於有序那部分。

共有兩種情況:

  • 如果左半部分有序,且目標值位於有序部分中,即target[nums[left],nums[mid])target\in[nums[left],nums[mid]),下次搜索區間可縮小爲[left,mid1][left,mid-1],反之搜索[mid+1,right][mid+1,right]

  • 如果右半部分有序,且目標值位於有序部分中,即target(nums[mid],nums[right])target\in(nums[mid],nums[right]),下次搜索區間可縮小爲[mid+1,right][mid+1,right],反之搜索[left,mid1][left,mid-1]

有一個幫助理解的key,即使關鍵值位於無序部分,下次二分查找還是要有上面的劃分過程,所以如果目標值存在,那麼最後一定是在一部分有序數組中被找到。

def search(nums, target):
    if not nums:  # 空數組
        return -1
    left, right = 0, len(nums) - 1
    while left <= right:
        mid = left + (right - left) // 2
        if nums[mid] == target:
            return mid
        if nums[0] <= nums[mid]:  #左半部分有序
            # 先判斷目標值是否存在有序的半個數組中
            if nums[0] <= target < nums[mid]: 
                right = mid - 1
            else:
                left = mid + 1
        elif nums[0] > nums[mid]: #右半部分有序
            if nums[mid] < target <= nums[-1]:
                left = mid + 1
            else:
                right = mid - 1
    return -1

34. 在排序數組中查找元素的第一個和最後一個位置

給定一個按照升序排列的整數數組 nums,和一個目標值 target。找出給定目標值在數組中的開始位置和結束位置。

注意:

  • 你的算法時間複雜度必須是 O(log n) 級別。
  • 如果數組中不存在目標值,返回 [-1, -1]。

同樣要求時間複雜度爲O(log n),所以優先考慮二分查找,可以看到這道題要返回的是一個範圍,先看示例一,如果只要求返回target的下標,那兩個8會返回哪一個呢?我測試了一下,示例一中這種情況是會返回4,也就是第二個8的下標;但如果刪去一個7,返回的是2,也就是第一個8的下標,所以返回數組中有重複元素的下標與數組長度有很大關係。

這裏就要用到二分查找關於收縮邊界的知識,也就是能固定返回數組中重複元素最左邊的一個或者最右邊的一個,不再受數組長度的影響,所以只要找到目標值左側邊界和右側邊界就可以得到答案。

這裏介紹一下左側邊界,右側類比。與普通框架不同的是,在找到目標時不返回,因爲此時找到的目標值不一定就是最左邊的一個,這時要收縮右側邊界,以便找到左側邊界,如果left指針和right指針相鄰,mid是一定指向left的,所以最後返回left即可,可以根據下面代碼推導一下示例一更容易理解。

def searchRange(nums,target):
    res = [-1,-1]
    if not nums: #空數組
        return res
    def leftbound(nums,target): #找到左側邊界
        left,right = 0,len(nums)-1
        while left<=right:
            mid = left + (right-left)//2
            if nums[mid]>target:
                right = mid-1
            elif nums[mid]<target:
                left = mid+1
            elif nums[mid] == target:
                right = mid-1 #不返回,收縮右側邊界
        #判斷是否越界
        if(left>=len(nums) or nums[left]!=target):
            return -1
        return left
    def rightbount(nums,target): # 找到右側邊界
        left,right = 0,len(nums)-1
        while left<=right:
            mid = left + (right-left)//2
            if nums[mid]>target:
                right = mid-1
            elif nums[mid]<target:
                left = mid+1
            elif nums[mid] == target:#不返回,收縮左側邊界
                left = mid+1
        #判斷是否越界
        if(right<0 or nums[right]!=target):
            return -1
        return right
    res[0] = leftbound(nums,target)
    res[1] = rightbount(nums,target)
    return res

74. 搜索二維矩陣

編寫一個高效的算法來判斷m×nm\times n矩陣中,是否存在一個目標值。

該矩陣具有如下特性:

  • 每行中的整數從左到右按升序排列。
  • 每行的第一個整數大於前一行的最後一個整數。

二分查找本來是不適用於二維矩陣的,但是這個矩陣有兩個特性,在這兩個特性的基礎上如果將二維數組的所有元素按照順序依次存入一個一維數組中,那麼這個一維數組就是有序的,因此也滿足二分查找的條件。

現在最大的問題就是如何通過新建的一維數組的索引在二維矩陣中找到對應的元素,這裏像是在於找規律?我也是從題解piao到的,比如16在一維數組中的下標爲6,那麼他在二維矩陣中的位置爲(6//n,6%n),剛好爲(1,2)。只要瞭解到這個規律,能準確的在矩陣中找到mid指針對應元素,剩下的就是普通升序數組的二分查找。

def searchMatrix(matrix,target):
    m = len(matrix)#行
    if m == 0:
        return False
    n = len(matrix[0])#列
    left,right = 0,m*n-1
    while left<=right:
        mid = left + (right-left)//2
        row = mid // n #元素在矩陣中的行
        col = mid % n #元素在矩陣中的列
        if matrix[row][col] == target:
            return True
        else:
            if matrix[row][col]>target:
                right = mid - 1
            else:
                left = mid + 1
    return False

二分查找比較難搞的地方就是邊緣問題,比如循環條件就有很多種表達方式,有帶等於號的,也有不帶的,我比較習慣用left<=rightleft<=right這一種,是否帶等於號又和right指針有關,這裏的細節感興趣可以自己查一下,形式一多就會相互混淆,容易出現越界的問題,所以找一種自己認爲好理解的形式,不要隨意更改。

關注公衆號【奶糖貓】可獲取更多精彩好文

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章