LeetCode 81,在不滿足二分的數組內使用二分法 II

本文始發於個人公衆號:TechFlow,原創不易,求個關注


今天是LeetCode專題第50篇文章,我們來聊聊LeetCode中的81題Search in Rotated Sorted ArrayII。

它的官方難度是Medium,點贊1251,反對470,通過率32.8%。從通過率上來看,這題屬於Medium難度當中偏難一些的題目,也的確如此,稍稍有些考驗思維。


題意


假設我們有一個含有重複元素的有序數組,我們隨意選擇一個位置將它分成兩半,然後將這兩個部分調換順序拼接成一個新的數組。現在給定一個target,要求返回一個bool結果,表明target是否在數組當中


樣例


Input: nums = [2,5,6,0,0,1,2], target = 0
Output: true
Input: nums = [2,5,6,0,0,1,2], target = 3
Output: false

如果是你按照順序刷LeetCode或者是本專題的話,你會發現我們在之前做過一道非常相似的題目。它就是LeetCode的33題,Search in Rotated Sorted ArrayI。不過不同的是,在33題的題意當中,明確表明了數組當中的元素是不包含重複元素的,除此之外,這兩題的題意完全一樣。

LeetCode 33,在不滿足二分的數組內使用二分的方法

這麼一點小小的差別會帶來解法的變化嗎?


題解


答案當然是肯定的,不然出題人可以退休了。

問題是,問題出在哪裏呢?

我們先不着急,先來回憶一下33題中的做法。我們當時使用了一個最簡單的笨辦法,就是先通過二分法找到數組截斷的位置。然後再通過截斷的位置還原出原數組的情況,根據我們target的大小,找到它可能存在的位置。

但是在當前這個問題當中,這個思路走不通了。走不通的原因也很簡單,就是因爲重複元素的存在。

舉個例子:[1, 3, 1, 1, 1, 1, 1, 1]

當我們進行二分查找的時候,發現mid是1和left的1相等,我們根本無法判斷截斷點究竟在mid的左側還是右側,二分查找也就無從談起了。

我們當然可以退一步採用遍歷的方法去尋找切分點,但是既然如此,我們爲什麼不直接去尋找答案呢?反正都已經是O(n)的複雜度了。所以這是行不通的,我們想要使得複雜度維持在O(logN)O(logN)就必須要尋找其他的路數。

思路和解法很多時候不是憑空來的,需要我們對問題進行深入的分析。在這個問題當中,我們的問題是明確並且簡單的。就是一個調換了部分順序的有序數組,只是我們不確定的是調換的部分究竟有多長。由於我們最終希望通過二分法來尋找答案,所以我們可以根據調換的元素是否過半想出兩種情況來。

我把這兩種情況用圖展示出來:

也就是說我們的分割點可能在數組的前半段也可能在後半段,對於這兩種情況我們的處理方法是不同的。

我們先看第一種情況,數組的前半段是有序的,後半段存在截斷。如果target的範圍在前半段當中,我們可以拋棄掉後半段,直接在前半段中進行二分。否則,我們需要捨棄前半段,在後半段當中重複這個過程。我們可以把後半段看成是一個全新的問題,也一樣可以分成兩種情況,類似於遞歸一樣的往下執行即可。

再來看第二種情況,第二種情況的後半段和第一種情況的前半段是一樣的,都是有序的元素,我們直接二分即可。它的前半段和第一種情況的後半段是一樣的,我們沒法判斷,需要繼續二分。

也就是說,我們只能在有序的數組進行二分,如果當前數組存在分段,不是整體有序的,那我們就對它進行拆分。拆分之後總能找到有序的部分,如果還找不到就繼續拆分。因爲分段點只有一個,所以不論當前的數組什麼樣,拆分一次之後,必然至少可以找到一段是有序的

想明白這點之後就簡單了,看起來很像是遞歸,但實際上它的本質仍然是二分。代碼並不難寫,但是還有一個問題沒解決,就是當nums[m] = nums[l]的時候,我們如何判斷是哪一種情況呢?

答案是沒法判斷,兩種情況都有可能,對於這種情況也沒有很好的辦法,我想出來的辦法是可以將l向右移動一位,相當於拋棄了一個最左側的數。我們把這些思路總結總結,代碼也就出來了:

class Solution:
    def search(self, nums: List[int], target: int) -> bool:
        l, r = 0, len(nums)-1
        while l <= r:
            m = (l + r) >> 1
            if nums[m] == target:
                return True
            
            if nums[l] == nums[m]:
                l += 1
                continue
                
            if nums[l] < nums[m]:
                if nums[l] <= target < nums[m]:
                    r = m - 1
                else:
                    l = m + 1
            else:
                if nums[m] < target <= nums[r]:
                    l = m + 1
                else:
                    r = m - 1
        return False

總結


到這裏,我們關於這道題的題解就結束了。在問題的最後,出題人給我們留了一個問題,和33題比起來,這題的解法的時間複雜度會有變化嗎

表面上看我們一樣用到了二分,所以同樣是log級的複雜度,問題的複雜度並沒有變化。但實際上並不是這樣的,我們來看一種最壞的情況,假設數組當中所有的值全部相等。這個時候二分就不起效果了,最終會退化成O(n)的線性枚舉,這樣又變成了O(n)的複雜度。當然,在大部分情況下,這並不會發生。所以是算法的最壞複雜度退化成了O(n),平均複雜度依然是O(logN)。

如果喜歡本文,可以的話,請點個關注,給我一點鼓勵,也方便獲取更多文章。

在這裏插入圖片描述

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