假設按照升序排序的數組在預先未知的某個點上進行了旋轉。
( 例如,數組 [0,1,2,4,5,6,7] 可能變爲 [4,5,6,7,0,1,2] )。
請找出其中最小的元素。
你可以假設數組中不存在重複元素。
示例 1:
輸入: [3,4,5,1,2]
輸出: 1
示例 2:
輸入: [4,5,6,7,0,1,2]
輸出: 0
這道尋找最小值的題目可以用二分查找法來解決,時間複雜度爲O(logN),空間複雜度爲O(1)。
看一下代碼:
class Solution {
public:
int findMin(vector<int>& nums) {
int left = 0;
int right = nums.size() - 1;
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] > nums[right]) {
left = mid + 1;
} else {
right = mid;
}
}
return nums[left];
}
};
首先說一下主要思路:
單調遞增的序列:
*
*
*
*
*
做了旋轉:
*
*
*
*
*
用二分法查找,需要始終將目標值(這裏是最小值)套住,並不斷收縮左邊界或右邊界。
左、中、右三個位置的值相比較,有以下幾種情況:
左值 < 中值, 中值 < 右值 :沒有旋轉,最小值在最左邊,可以收縮右邊界
右
中
左
左值 > 中值, 中值 < 右值 :有旋轉,最小值在左半邊,可以收縮右邊界
左
右
中
左值 < 中值, 中值 > 右值 :有旋轉,最小值在右半邊,可以收縮左邊界
中
左
右
左值 > 中值, 中值 > 右值 :單調遞減,不可能出現
左
中
右
分析前面三種可能的情況,會發現情況1、2是一類,情況3是另一類。
如果中值 < 右值,則最小值在左半邊,可以收縮右邊界。
如果中值 > 右值,則最小值在右半邊,可以收縮左邊界。
通過比較中值與右值,可以確定最小值的位置範圍,從而決定邊界收縮的方向。
而情況1與情況3都是左值 < 中值,但是最小值位置範圍卻不同,這說明,如果只比較左值與中值,不能確定最小值的位置範圍。
所以我們需要通過比較中值與右值來確定最小值的位置範圍,進而確定邊界收縮的方向。
接着分析解法裏的一些問題:
首先是while循環裏的細節問題。
這裏的循環不變式是left < right, 並且要保證左閉右開區間裏面始終套住最小值。
中間位置的計算:mid = left + (right - left) / 2
這裏整數除法是向下取整的地板除,mid更靠近left,
再結合while循環的條件left < right,
可以知道left <= mid,mid < right,
即在while循環內,mid始終小於right。
因此在while循環內,nums[mid]要麼大於要麼小於nums[right],不會等於。
這樣else {right = mid;}這句判斷可以改爲更精確的
else if (nums[mid] < nums[right]) {right = mid;}。
再分析一下while循環退出的條件。
如果輸入數組只有一個數,左右邊界位置重合,left == right,不會進入while循環,直接輸出。
如果輸入數組多於一個數,循環到最後,會只剩兩個數,nums[left] == nums[mid],以及nums[right],這裏的位置left == mid == right - 1。
如果nums[left] == nums[mid] > nums[right],則左邊大、右邊小,
需要執行left = mid + 1,使得left == right,左右邊界位置重合,循環結束,nums[left]與nums[right]都保存了最小值。
如果nums[left] == nums[mid] < nums[right],則左邊小、右邊大,
會執行right = mid,使得left == right,左右邊界位置重合,循環結束,nums[left]、nums[mid]、nums[right]都保存了最小值。
細化了的代碼:
class Solution {
public:
int findMin(vector<int>& nums) {
int left = 0;
int right = nums.size() - 1; /* 左閉右閉區間,如果用右開區間則不方便判斷右值 */
while (left < right) { /* 循環不變式,如果left == right,則循環結束 */
int mid = left + (right - left) / 2; /* 地板除,mid更靠近left */
if (nums[mid] > nums[right]) { /* 中值 > 右值,最小值在右半邊,收縮左邊界 */
left = mid + 1; /* 因爲中值 > 右值,中值肯定不是最小值,左邊界可以跨過mid */
} else if (nums[mid] < nums[right]) { /* 明確中值 < 右值,最小值在左半邊,收縮右邊界 */
right = mid; /* 因爲中值 < 右值,中值也可能是最小值,右邊界只能取到mid處 */
}
}
return nums[left]; /* 循環結束,left == right,最小值輸出nums[left]或nums[right]均可 */
}
};
作者:armeria-program
鏈接:https://leetcode-cn.com/problems/find-minimum-in-rotated-sorted-array/solution/er-fen-cha-zhao-wei-shi-yao-zuo-you-bu-dui-cheng-z/
來源:力扣(LeetCode)
著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。
再討論一個問題:
爲什麼左右不對稱?爲什麼比較mid與right而不比較mid與left?能不能通過比較mid與left來解決問題?
左右不對稱的原因是:
這是循環前升序排列的數,左邊的數小,右邊的數大,而且我們要找的是最小值,肯定是偏向左找,所以左右不對稱了。
爲什麼比較mid與right而不比較mid與left?
具體原因前面已經分析過了,簡單講就是因爲我們找最小值,要偏向左找,目標值右邊的情況會比較簡單,容易區分,所以比較mid與right而不比較mid與left。
那麼能不能通過比較mid與left來解決問題?
能,轉換思路,不直接找最小值,而是先找最大值,最大值偏右,可以通過比較mid與left來找到最大值,最大值向右移動一位就是最小值了(需要考慮最大值在最右邊的情況,右移一位後對數組長度取餘)。
以下是先找最大值的代碼,可以與前面找最小值的比較:
class Solution {
public:
int findMin(vector<int>& nums) {
int left = 0;
int right = nums.size() - 1;
while (left < right) {
int mid = left + (right - left + 1) / 2; /* 先加一再除,mid更靠近右邊的right */
if (nums[left] < nums[mid]) {
left = mid; /* 向右移動左邊界 */
} else if (nums[left] > nums[mid]) {
right = mid - 1; /* 向左移動右邊界 */
}
}
return nums[(right + 1) % nums.size()]; /* 最大值向右移動一位就是最小值了(需要考慮最大值在最右邊的情況,右移一位後對數組長度取餘) */
}
};
使用left < right作while循環條件可以很方便推廣到數組中有重複元素的情況,即154題:
https://leetcode-cn.com/problems/find-minimum-in-rotated-sorted-array-ii/
只需要在nums[mid] == nums[right]時挪動右邊界就行:
class Solution {
public:
int findMin(vector<int>& nums) {
int left = 0;
int right = nums.size() - 1;
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] > nums[right]) {
left = mid + 1;
} else if (nums[mid] < nums[right]) {
right = mid;
} else {
right--;
}
}
return nums[left];
}
};
初始條件是左閉右閉區間,right = nums.size() - 1,
那麼能否將while循環的條件也選爲左閉右閉區間left <= right?
可以的,代碼如下:
class Solution {
public:
int findMin(vector<int>& nums) {
int left = 0;
int right = nums.size() - 1;
while (left <= right) { // 循環的條件選爲左閉右閉區間left <= right
int mid = left + (right - left) / 2;
if (nums[mid] >= nums[right]) { // 注意是當中值大於等於右值時,
left = mid + 1; // 將左邊界移動到中值的右邊
} else { // 當中值小於右值時
right = mid; // 將右邊界移動到中值處
}
}
return nums[right]; // 最小值返回nums[right]
}
};
這道題還有其它解法:
始終將nums[mid]
與最右邊界的數進行比較,相當於在每次裁剪區間之後始終將最右邊的數附在新數組的最右邊。
class Solution {
public:
int findMin(vector<int>& nums) {
int right_boundary = nums[nums.size() - 1];
int left = 0;
int right = nums.size() - 1;
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] > right_boundary) {
left = mid + 1;
} else {
right = mid;
}
}
return nums[left];
}
};
或者在處理了第一種情況之後,始終將nums[mid]與最左邊界的數nums[0]進行比較,即相當於在每次裁剪區間之後始終將最左邊的數附在新數組的最左邊,再不斷處理情況2及情況3。
class Solution {
public:
int findMin(vector<int>& nums) {
int left = 0;
int right = nums.size() - 1;
if (nums[0] < nums[right])
return nums[left];
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[0] > nums[mid])
right = mid;
else
left = mid + 1;
}
return nums[left];
}
};
作者:armeria-program
鏈接:https://leetcode-cn.com/problems/find-minimum-in-rotated-sorted-array/solution/er-fen-cha-zhao-wei-shi-yao-zuo-you-bu-dui-cheng-z/
來源:力扣(LeetCode)
著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。