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題的題意當中,明確表明了陣列當中的元素是不包含重複元素的,除此之外,這兩題的題意完全一樣。
這麼一點小小的差別會帶來解法的變化嗎?
題解
答案當然是肯定的,不然出題人可以退休了。
問題是,問題出在哪裡呢?
我們先不著急,先來回憶一下33題中的做法。我們當時使用了一個最簡單的笨辦法,就是先通過二分法找到陣列截斷的位置。然後再通過截斷的位置還原出原陣列的情況,根據我們target的大小,找到它可能存在的位置。
但是在當前這個問題當中,這個思路走不通了。走不通的原因也很簡單,就是因為重複元素的存在。
舉個例子:[1, 3, 1, 1, 1, 1, 1, 1]
當我們進行二分查詢的時候,發現mid是1和left的1相等,我們根本無法判斷截斷點究竟在mid的左側還是右側,二分查詢也就無從談起了。
我們當然可以退一步採用遍歷的方法去尋找切分點,但是既然如此,我們為什麼不直接去尋找答案呢?反正都已經是O(n)的複雜度了。所以這是行不通的,我們想要使得複雜度維持在
思路和解法很多時候不是憑空來的,需要我們對問題進行深入的分析。在這個問題當中,我們的問題是明確並且簡單的。就是一個調換了部分順序的有序陣列,只是我們不確定的是調換的部分究竟有多長。由於我們最終希望通過二分法來尋找答案,所以我們可以根據調換的元素是否過半想出兩種情況來。
我把這兩種情況用圖展示出來:
也就是說我們的分割點可能在陣列的前半段也可能在後半段,對於這兩種情況我們的處理方法是不同的。
我們先看第一種情況,陣列的前半段是有序的,後半段存在截斷。如果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)。
如果喜歡本文,可以的話,請點個關注,給我一點鼓勵,也方便獲取更多文章。
本文使用 mdnice 排版