[LeetCode] #33. Search in Rotated Sorted Array

題目

There is an integer array nums sorted in ascending order (with distinct values).

Prior to being passed to your function, nums is rotated at an unknown pivot index k (0 <= k < nums.length) such that the resulting array is [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]] (0-indexed). For example, [0,1,2,4,5,6,7] might be rotated at pivot index 3 and become [4,5,6,7,0,1,2].

Given the array nums after the rotation and an integer target, return the index of target if it is in nums, or -1 if it is not in nums.

You must write an algorithm with O(log n) runtime complexity.

直覺解

構想:

用二分法找到中斷點(沒有依照遞增排列的那個點),例如 [4,5,6,7,0,1,2] 的中斷點的 index 是 4,如果在尋找中斷點的過程中就先發現 target,則直接回傳 target 的 index。判斷 target 在中斷點的左邊還是右邊後,再用二分法找 target 的 index。

為了避免尋找中斷點時,index 超出 list 的範圍,長度是 1 的 list 就不執行 find_pivot 函式了。

我的變數命名的不太好,雖然取名叫 pivot,但是意思跟題目說的 pivot 不一樣。以 [4,5,6,7,0,1,2] 而言,題目說的 pivot 是指「[0,1,2,4,5,6,7] is rotated at pivot index 3」,我的意思則是「在 index 4 的地方,[4,5,6,7,0,1,2] 被切成了兩段遞增數列」。

python 3 實作:

class Solution: 
    def search(self, nums: List[int], target: int) -> int:
        if target < nums[0] and target > nums[-1]:
            return -1

        if len(nums) == 1:
            if nums[0] == target:
                return 0
            return -1

        target_index, pivot = self.find_pivot(nums, target)
        if target_index is not None:
            return target_index
        min = 0
        max = len(nums) - 1
        if target < nums[0]:
            min = pivot + 1
        else:
            max = pivot - 1

        while min <= max:
            mid = (min + max) // 2
            if nums[mid] == target:
                target_index = mid
                break
            elif nums[mid] < target:
                min = mid + 1
            else:
                max = mid - 1
        else:
            target_index = -1

        return target_index

    def find_pivot(self, nums: List[int], target: int) -> tuple:
        target_index = None
        pivot = None
        # no pivot
        if nums[-1] > nums[0]:
            return (target_index, len(nums))

        # there is a pivot
        min = 0
        max = len(nums) - 1
        while min <= max:
            mid = (min + max) // 2
            if nums[mid] == target:
                target_index = mid
                break

            if nums[mid] < nums[mid-1]:
                pivot = mid
                break

            if nums[mid] < nums[0]:
                max = mid - 1
            else:
                min = mid + 1

        return (target_index, pivot)

效能:

Runtime: 44 ms, faster than 43.18% of Python3 online submissions for Search in Rotated Sorted Array.
Memory Usage: 14.6 MB, less than 55.97% of Python3 online submissions for Search in Rotated Sorted Array.

實作過程中犯過的錯誤

  • 我的寫法預設有中斷點,但有些 nums 沒有中斷點,例如 [1,2,3,4,5,6]

  • 中斷點的判斷方式錯誤。原本以為「中斷點比左右兩邊的數都小」;後來發現不用那麼複雜,只要「中斷點比左邊的數小」就夠了。

研究別人的解

來源:sample 40 ms submission

研究重點:我的寫法又臭又長,想看更簡潔的寫法。

別人的 python 3 實作:

class Solution: 
    def search(self, nums: List[int], target: int) -> int:
        l, r = 0, len(nums) - 1
        while l + 1 < r:
            mid = (l + r) // 2
            if target == nums[mid]:
                return mid

            if nums[mid] > nums[l]:
                if nums[l] <= target < nums[mid]:
                    r = mid
                else:
                    l = mid
            else:
                if nums[mid] < target <= nums[r]:
                    l = mid
                else:
                    r = mid
        if nums[l] == target:
            return l
        elif nums[r] == target:
            return r
        else:
            return -1

效能:

Runtime: 40 ms, faster than 72.88% of Python3 online submissions for Search in Rotated Sorted Array.
Memory Usage: 14.7 MB, less than 55.97% of Python3 online submissions for Search in Rotated Sorted Array.

研究感想

nums 可能會從中斷點處,被切成兩段嚴格遞增數列。右邊那段遞增一定比左邊那段小。把他的其中一段程式碼拿出來看:

if nums[mid] > nums[l]: # 代表 l 和 mid 在同一段遞增裡面
    if nums[l] <= target < nums[mid]: # 代表 target 和 l , mid 在同一段遞增裡面
        r = mid
    else: # 不確定 target 和 r 是否在「l 和 mid 所在的那段遞增」裡面,
          # 但反正一定不會在「l~mid」這個範圍裡
        l = mid
else: # 代表 l 在左段遞增裡,mid 在右段遞增裡
    if nums[mid] < target <= nums[r]: # 代表 target 和 mid, r 在同一段遞增裡面
        l = mid
    else: # 不確定 target 和 l 是否在「mid 和 r 所在的那段遞增」裡面,
          # 但反正一定不會在「mid~r」這個範圍裡
        r = mid

取了 mid 以後, l~mid 或 mid~r 這兩個範圍的其中一個,一定是在同一段嚴格遞增裡面。根據 target 是否在這段嚴格遞增裡面,決定縮減右邊範圍還是左邊範圍。

以前寫的題目,l, r, target 都在同一段嚴格遞增裡面,所以我的直覺解也習慣性地想要保持讓 l, r, target 都在同一段嚴格遞增裡面,所以非得要先查出中斷點在哪裡,我才有辦法設定 l 和 r 的 index。但是看完這個人的解,我才發現,原來,即使沒有「l, r, target 都在同一段嚴格遞增裡面」這樣的情報,也還是能找到 target。原來要找到 target 所需的情報,比我原本以為的還要少。我真是開了眼界了!

腦洞:如果被切成三段嚴格遞增的話,要怎麼找到 target?例如 [8,9,10,4,5,6,7,0,1,2] 之類的?

while 迴圈後面的 if- elif- else 可不可以拿掉

為什麼 while 的條件是 l + 1 < r(而不是 l <= r 或 l < r)。一開始我以為,這是為了避免 l, r, mid 超出 nums 的 index 範圍,不過仔細看的話,他的寫法, 是不會超出 index 範圍的。我試著把他的程式碼改成這樣:

class Solution: 
    def search(self, nums: List[int], target: int) -> int:
        l, r = 0, len(nums) - 1
        while l <= r:
            mid = (l + r) // 2
            if target == nums[mid]:
                return mid

            if nums[mid] > nums[l]:
                if nums[l] <= target < nums[mid]:
                    r = mid - 1
                else:
                    l = mid + 1
            else:
                if nums[mid] < target <= nums[r]:
                    l = mid + 1
                else:
                    r = mid - 1
        return -1

Wrong Answer

Input:
[3,1]
1

Output: -1

Expected: 1

r 變成 -1 結果就跳出迴圈 return -1 了。

參考下方的 sample 28 ms submission 再次改良,終於成功把 if- elif- else 拿掉了,關鍵點在於把 nums[mid] > nums[l] 改成 nums[mid] >= nums[l]

class Solution: 
    def search(self, nums: List[int], target: int) -> int:
        l, r = 0, len(nums) - 1
        while l <= r:
            mid = (l + r) // 2
            if target == nums[mid]:
                return mid

            if nums[mid] >= nums[l]:
                if nums[l] <= target < nums[mid]:
                    r = mid - 1
                else:
                    l = mid + 1
            else:
                if nums[mid] < target <= nums[r]:
                    l = mid + 1
                else:
                    r = mid - 1
        return -1

效能:

Runtime: 32 ms, faster than 97.67% of Python3 online submissions for Search in Rotated Sorted Array.
Memory Usage: 14.7 MB, less than 55.97% of Python3 online submissions for Search in Rotated Sorted Array.

多一個等號,就從原本的 r = mid - 1 變成 l = mid + 1。一個等號就關係著能不能把 if- elif- else 拿掉,真是精細。

研究別人的解

來源:sample 28 ms submission

研究重點:上一個解,while 迴圈後面的 if- elif- else 可不可以拿掉。

別人的 python 3 實作:

class Solution: 
    def search(self, nums: List[int], target: int) -> int:
        left, right = 0, len(nums) - 1

        while left <= right:
            mid = (left + right) // 2

            if nums[mid] == target:
                return mid

            if nums[left] <= nums[mid]:
                if nums[left] <= target <= nums[mid]:
                    right = mid - 1
                else:
                    left = mid + 1

            else:
                if nums[mid] <= target <= nums[right]:
                    left = mid + 1
                else:
                    right = mid - 1

        else:
            return -1

效能:

Runtime: 44 ms, faster than 43.98% of Python3 online submissions for Search in Rotated Sorted Array.
Memory Usage: 14.7 MB, less than 24.23% of Python3 online submissions for Search in Rotated Sorted Array.

Comments

Popular posts from this blog

Alpha Camp 全端開發課程學習心得

在 javascript 用 regular expression 為金額加上千位數分隔符號

shop_platform - sqlalchemy.exc.TimeoutError