题目
题目来源:leetcode 剑指Offer 11:旋转数组的最小数字
把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个递增排序的数组的一个旋转,输出旋转数组的最小元素。
示例1:
输入: [3,4,5,1,2]
输出: 1
示例2:
输入: [2,2,2,0,1]
输出: 0
示例3:
输入: [2,3,5]
输出: 2
解题思路
旋转前的数组是单调不减的,旋转后的数组会出现0个(将数组中的前0个元素移动到后面)或1个断崖。若没有断崖,数组中第一个元素就是最小值;若有,旋转后的数组仅在断崖处是递减的,且较小值(崖底)是数组的最小值。
没有断崖:
有断崖:
使用二分查找在数组numbers
下标[left..right]
序列内寻找断崖(或判断是否存在断崖)。如果中间元素numbers[mid] < numbers[mid - 1]
,则mid
对应着数组的最小值。
如果numbers[mid]
大于numbers[right]
,说明断崖在右半部。
如果numbers[mid]
小于numbers[right]
,说明断崖在左半部。
如果numbers[mid] == numbers[right]
,则无法判断断崖的位置,需要将right
减1
,重新做判断。
断崖在右半部:
断崖(最小值)在左半部:
如果left > right
时还未找到最小值,该数组无断崖。
代码实现(C++)
int minArray(vector<int>& numbers) {
if (numbers.size() < 2) {
return numbers.front();
}
int left = 0;
int right = numbers.size() - 1;
// 二分查找
while (left <= right) {
int mid = left + ((right - left) >> 1);
// 找到了断崖
if (mid > 0 && numbers[mid] < numbers[mid - 1]) {
return numbers[mid];
}
if (numbers[mid] == numbers[right]) {
--right;
}
else if (numbers[mid] < numbers[right]) {
right = mid - 1;
}
else {
left = mid + 1;
}
}
// 没有断崖
// 使用"return numbers[left];"语句也能通过所有的测试用例
return numbers.front();
}
以下代码也能通过所有的测试用例。
class Solution {
public:
int minArray(vector<int>& numbers) {
if (numbers.size() < 2) {
return numbers.front();
}
int left = 0;
int right = numbers.size() - 1;
while (left < right) { // 去掉了=符号
int mid = left + ((right - left) >> 1);
if (mid > 0 && numbers[mid] < numbers[mid - 1]) {
return numbers[mid];
}
if (numbers[mid] == numbers[right]) {
--right;
}
else if (numbers[mid] < numbers[right]) {
right = mid - 1;
}
else {
left = mid + 1;
}
}
return numbers[left];
}
};
二分查找
如果在数组下标[left..right]
范围内根据某种要求进行二分查找,笔者原以为while
循环控制条件left < right
和left <= right
是等价的,只要最后return left
,那结果就是正确的。通过本题可知,在旋转的有序数组上进行二分查找时循环控制语句left < right
和left <= right
是等价的。
但事实上,如果循环控制条件是left < right
,当left == right
时退出循环,而此时left
所对应的元素未必满足要求。因此正确的循环控制条件应该是left <= right
。
例如要在数组nums=[1,3,5]
中找到第一个大于等于2的元素,开始left = 0,right = 2,mid = 1
,因为nums[1]
符合要求,因此将right
置为0
,此时left == right
。如果循环控制语句是left < right
,那将返回nums[0]
,得到错误的结果;如果循环控制语句是left <= right
,那将进一步对nums[0]
进行判断,最后返回nums[1]
,结果正确。
由此可见,分别在正常的有序数组和旋转后的有序数组上使用二分查找时,两者有一些微妙的区别。
扩展
要在旋转的有序数组上进行查找操作,二分查找是首先要考虑的方法。但由于旋转数组是局部有序的,所以要区分有序段和无序段(存在断崖),还要注意出现重复元素。
leetcode 面试题10.03:搜索旋转数组是在旋转数组中搜索第一个出现的元素,除了二分查找,还用到了递归的思路。