153.寻找旋转排序数组中的最小值

55 阅读4分钟

题目:

已知一个长度为 n 的数组,预先按照升序排列,经由 1n旋转 后,得到输入数组。例如,原数组 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.length
  • 1 <= n <= 5000
  • -5000 <= nums[i] <= 5000
  • nums 中的所有整数 互不相同
  • 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 后仍为 0left 也不变 → 无限循环

数组 nums = [3,4,5,1,2](演示正常收敛):

  • left=0,right=4,mid=2 → nums[2]>nums[4] → left=mid+1=3
  • left=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 来记录最小值;但这样实现更容易出错且不直观。