题目:
已知一个长度为 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次旋转
代码:
关于为循环条件为何不是left<=right的理解:
好问题 — 这个细节很常把人绕糊涂。以下把原因、不同行为的区别和若干例子讲清楚 —— 目的是让你能既安全又正确地选择 while 条件与边界更新。
核心结论(先说结论)
对于常见且正确的解法
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] <= nums[right]) right = mid;
else left = mid + 1;
}
return nums[left];
必须用 left < right(而 不是 left <= right)。
原因是:我们在某个分支里使用 right = mid(不减 1),这是为了保留 mid 可能是最小值的可能性。而一旦循环条件是 left <= right,当 left == right 时循环仍会执行,而 right = mid 在 mid == right == left 时不会使区间缩小,从而可能导致死循环。
详细解释(一步步推导)
1) 我们的目标与不变式
设不变式:当前最小值一定在区间 [left, right](包含两端)。初始成立。每次循环要保证:
- 不变式仍成立;
- 区间
[left,right]的长度必须减少(至少减 1),这样才保证最终收敛(终止)。
2) 选 right = mid(而不是 mid-1)的原因
当 nums[mid] <= nums[right] 时,mid 自身可能就是最小值(比如:[0,1,2,3],mid 指向 0)。所以必须把 mid包含在下一轮的候选区间里,故 right = mid(而非 mid-1,否则可能把正确答案跳过)。
3) 为什么 while (left < right) 与 right = mid 搭配是安全的
当 left < right 时,mid = left + (right-left)/2 总满足 mid < right(因为 right-left >= 1 -> mid <= right-1)。因此执行 right = mid 会使 right 严格减小(区间长度缩小),或者执行 left = mid+1 会使 left 严格增大。也就是说每次循环都会缩小区间,最终收敛到 left == right,循环停止,返回 nums[left]。
4) 如果改成 while (left <= right) 会怎样?
当 left == right 时,循环体仍会执行一次。此时 mid == left == right,若你的分支仍是 right = mid,那么 right 不会改变(仍等于 mid),left 也不改变 → 区间不缩小,陷入死循环。
你可能改成 right = mid - 1 来保证缩小,但那会把 mid 从候选集中去掉,可能跳过真实的最小值(因为当 nums[mid] 是最小值时,right = mid-1 就错了)。要在 left <= right 下正确做法通常需要另外维护一个 ans并在每次循环时更新 ans = min(ans, nums[mid]),然后把区间严格收缩(用 mid-1 或 mid+1)。这种方式可以做到不死循环但更复杂,也更容易出错。
简短例子说明(关键的反例)
数组 nums = [1]:
- 用
while (left < right):left=0,right=0,条件不成立,直接返回nums[0]=1—— 正确且终止。 - 用
while (left <= right)且在分支里做right = mid:第一次进入时left=0,right=0,mid=0,执行right=mid后仍为0,left也不变 → 无限循环。
数组 nums = [3,4,5,1,2](演示正常收敛):
left=0,right=4,mid=2→nums[2]>nums[4]→left=mid+1=3left=3,right=4,mid=3→nums[3]<=nums[4]→right=mid=3- 现在
left==right==3,退出循环,返回nums[3]=1—— 正确。
小结(实践建议)
- 对于“寻找旋转数组最小值”这种需要保留 mid 作为候选的场景,优选写法是:
while (left < right)+if (nums[mid] <= nums[right]) right = mid; else left = mid + 1;
这套组合既容易证明不变式,又保证每步区间严格缩小,安全可靠。 - 如果你非要用
while (left <= right),必须保证在每个分支都会严格移动left或right(比如把right设为mid-1),并且通常要额外维护一个ans来记录最小值;但这样实现更容易出错且不直观。