题目描述
已知一个长度为n的数组,预先按照升序排列,经由1到n次旋转后,得到输入数组。例如,原数组nums = [0,1,2,4,5,6,7]在变化后可能得到:
- 若旋转
4次,则可以得到[4,5,6,7,0,1,2] - 若旋转
7次,则可以得到[0,1,2,4,5,6,7]
注意,数组[a[0], a[1], a[2], ..., a[n-1]]旋转一次的结果为数组[a[n-1], a[0], a[1], a[2], ..., a[n-2]]。
给你一个元素值互不相同的数组nums,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的最小元素。
你必须设计一个时间复杂度为O(log n)的算法解决此问题。
示例 1:
输入:nums = [3,4,5,1,2]输出:1解释:原数组为 [1,2,3,4,5] ,旋转 3 次得到输入数组。
示例 2:
输入:nums = [4,5,6,7,0,1,2]输出:0解释:原数组为 [0,1,2,4,5,6,7] ,旋转 4 次得到输入数组。
示例 3:
输入:nums = [11,13,15,17]输出:11解释:原数组为 [11,13,15,17] ,旋转 4 次得到输入数组。
提示:
n == nums.length1 <= n <= 5000-5000 <= nums[i] <= 5000nums中的所有整数互不相同nums原来是一个升序排序的数组,并进行了1至n次旋转
解决方案:
代码逻辑拆解
边界初始化:
len = nums.size():获取数组长度;left = -1、right = len - 1:延续「开区间(left, right)」的二分查找设计,初始查找范围是(-1, len-1),这种设计能避免处理数组首尾时的越界问题;mid = 0:用于存储二分查找的中间索引。
二分循环条件:
while(left + 1 < right):这是开区间二分的经典终止条件,保证(left, right)区间内还有至少一个元素可检查;当left + 1 == right时,区间内无剩余元素,循环终止。
核心的区间收缩逻辑(关键):旋转排序数组的核心特性是:数组被旋转后分成左右两个升序子数组,且左侧子数组的所有元素 ≥ 右侧子数组的所有元素,而
nums[len-1]是右侧子数组的最后一个元素(也是右侧子数组的最大值)。基于此:- 计算中间位置
mid = (left + right) / 2; - 若
nums[mid] < nums[len-1]:说明mid落在右侧的升序子数组(最小值所在的子数组),最小值一定在mid左侧(包括mid),因此将right = mid缩小区间; - 否则(
nums[mid] ≥ nums[len-1]):说明mid落在左侧的升序子数组(数值更大的子数组),最小值一定在mid右侧,因此将left = mid缩小区间。
- 计算中间位置
结果返回:循环结束时
left + 1 == right,此时right恰好指向数组最小元素的索引,返回nums[right]即可得到最小值。
示例验证
以nums = [4,5,6,1,2,3]为例:
- 初始:
left=-1,right=5(nums[5]=3); - 第一次循环:
mid=2,nums[2]=6 > 3→left=2; - 第二次循环:
mid=3,nums[3]=1 < 3→right=3; - 此时
left+1=3 == right=3,循环终止,返回nums[3]=1(正确)。
总结
- 算法利用旋转排序数组的特性(最后一个元素区分左右升序子数组),通过二分查找将时间复杂度优化至
O(log n); - 「开区间
(left, right)」的边界设计简化了边界处理,无需额外判断数组是否旋转; - 核心判断条件是比较
nums[mid]和nums[len-1],以此确定最小值所在区间,最终right指向最小值索引。
函数源码:
class Solution { public: int findMin(vector<int>& nums) { int len=nums.size(); int right=len-1; int left=-1; int mid=0; while(left+1<right){ mid=(right+left)/2; if(nums[mid]<nums[len-1]){ right=mid; }else{ left=mid; } } return nums[right]; } };