轉載自:leetcode題解區-一文解決 4 道「搜索旋轉排序數組」題
本文涉及 4 道「搜索旋轉排序數組」題:
- LeetCode 33 題:搜索旋轉排序數組
- LeetCode 81 題:搜索旋轉排序數組-ii
- LeetCode 153 題:尋找旋轉排序數組中的最小值
- LeetCode 154 題:尋找旋轉排序數組中的最小值-ii
可以分為 3 類:
- 33、81 題:搜索特定值
- 153、154 題:搜索最小值
- 81、154 題:包含重復元素
33. 搜索旋轉排序數組
題目要求時間復雜度$O(logn)$,顯然應該使用二分查找。二分查找的過程就是不斷收縮左右邊界,而怎么縮小區間是關鍵。
如果數組「未旋轉」,在數組中查找一個特定元素 target
的過程為:
- 若
target == nums[mid]
,直接返回 - 若
target < nums[mid]
,則target
位於左側區間[left,mid)
中。令right = mid-1
,在左側區間查找 - 若
target > nums[mid]
,則target
位於右側區間(mid,right]
中。令left = mid+1
,在右側區間查找
但是這道題,由於數組「被旋轉」,所以左側或者右側區間不一定是連續的。在這種情況下,如何判斷 target
位於哪個區間?
首先,一個重要的結論:將區間分均分,必然有一半有序,一半無序。問題是如何找到有序的那一半?
根據旋轉數組的特性,當元素不重復時,如果 nums[i] <= nums[j]
,說明區間 [i,j]
是「連續遞增」的。
因此,在旋轉排序數組中查找一個特定元素時:
- 若
target == nums[mid]
,直接返回 - 若
nums[left] <= nums[mid]
,說明左側區間[left,mid]
「連續遞增」。此時:- 若
nums[left] <= target < nums[mid]
,說明target
位於左側。令right = mid-1
,在左側區間查找 - 否則,令
left = mid+1
,在右側區間查找
- 若
- 否則,說明右側區間
[mid,right]
「連續遞增」。此時:- 若
nums[mid] < target <= nums[right]
,說明target
位於右側區間。令left = mid+1
,在右側區間查找 - 否則,令
right = mid-1
,在左側區間查找
- 若
- 注意:區間收縮時不包含
mid
,也就是說,實際收縮后的區間是[left,mid)
或者(mid,right]
可以很容易地寫出代碼:
int search(vector<int>& nums, int target) { int left = 0, right = nums.size()-1, mid; while(left <= right) { int mid = (left+right) >> 1; if(nums[mid] == target) return mid; if(nums[left] <= nums[mid]) { if(nums[left] <= target && target < nums[mid]) right = mid-1; else left = mid+1; } else { if(nums[mid] < target && target <= nums[right]) left = mid+1; else right = mid-1; } } return -1; }
81. 搜索旋轉排序數組-ii
這道題是 33 題的升級版,元素可以重復。當 nums[left] == nums[mid]
時,無法判斷 target
位於左側還是右側,此時無法縮小區間,退化為順序查找。
例如 [1, 3, 1, 1, 1]中查找3,按原來的代碼就會出錯。
順序查找的一種方法是直接遍歷 [left,right]
每一項:
if nums[left] == nums[mid] { for i := left; i <= right; i++ { if nums[i] == target { return i } }
另一種方法是令 left++
,去掉一個干擾項,本質上還是順序查找:
if nums[left] == nums[mid] { left++ continue }
其實這道題沒有低於O(n)的算法,所以直接遍歷一遍即可。
153. 搜索旋轉排序數組中的最小值
如果數組沒有翻轉,即 nums[left] <= nums[right]
,則 nums[left]
就是最小值,直接返回。
如果數組翻轉,需要找到數組中第二部分的第一個元素:
下面討論數組翻轉的情況下,如何收縮區間以找到這個元素:
- 若
nums[left] <= nums[mid]
,說明區間[left,mid]
連續遞增,則最小元素一定不在這個區間里,可以直接排除。因此,令left = mid+1
,在[mid+1,right]
繼續查找
- 否則,說明區間
[left,mid]
不連續,則最小元素一定在這個區間里。因此,令right = mid
,在[left,mid]
繼續查找 [left,right]
表示當前搜索的區間。注意right
更新時會被設為mid
而不是mid-1
,因為mid
無法被排除。這一點和「33 題 查找特定元素」是不同的
int findMin(vector<int>& nums) { int left = 0, right = nums.size()-1, mid; while(left <= right) { if(nums[left] <= nums[right]) return nums[left]; // 如果整個區域遞增 mid = (left + right)>>1; if(nums[left] <= nums[mid]) left = mid+1; else right = mid; // mid是有可能的 } return -1; }
154. 搜索旋轉排序數組中的最小值-ii
這道題是 153 題的升級版,元素可以重復。和 81 題一樣,當 nums[left] == nums[mid]
時,退化為順序查找。
81 題提供了兩種方法:
- 一種是直接遍歷
[left,right]
每一項 - 另一種是
left++
,跳過一個干擾項
154 題只能使用第一種方法。因為如果 left
是最小元素,那么 left++
就把正確結果給跳過了。