轉載自: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++ 就把正確結果給跳過了。
