旋转数组的最小值:看 mid 和 right 谁更大
最小值是唯一让数组下降的那一格。mid 比右端大,断点还在右边;否则从 mid 到右端已经升序,断点在左边含 mid。
旋转数组的最小值是唯一「从大到小」的断点。二分比较 nums[mid] 与 nums[right]:若 nums[mid] > nums[right],断点在 mid 右侧,left = mid+1;否则从 mid 到 right 升序,断点在左半(含 mid),right = mid。收缩到 left == right 时即最小值。时间 O(log n),空间 O(1)。
这是 LeetCode 153. Find Minimum in Rotated Sorted Array。原本升序、互不相同的数组在某处旋转,例如 [1,2,3,4,5] 转成 [3,4,5,1,2]。求旋转后的最小值。要求 O(log n)。
主例 nums = [3, 4, 5, 1, 2],答案 1。没旋转时最小值就是 nums[0];旋转发生在最后一格时,最小值仍是原数组的头,被转到了中间某处。
线性扫一遍能做对,但浪费了「只有一个断点」这件事。普通二分找固定值也不对——这里没有 target。缺的是:中点这一侧还升序,还是已经跨过了下降。本文用 mid 和 right 的大小关系决定往哪半走。
用 nums[mid] 和 nums[right] 判断方向
旋转后数组是「一段大的升序 + 一段小的升序」。唯一下降发生在两段接头:某个较大的数后面突然接到原数组的最小值。最小值就是这个断点。第一直觉从左往右找第一处下降,O(n) 能做对。
缺口是丢掉一半的依据。拿 mid 和 right 比,而不是和 left 比。nums[mid] > nums[right]:mid 还在大段上,right 已经掉进小段,下降发生在 mid 与 right 之间,最小值严格在 mid 右边,left = mid+1。nums[mid] < nums[right]:从 mid 到 right 没有下降,整段升序,最小值不会在 mid 右边,但 mid 自己可能就是最小值,right = mid。元素互不相同,不会出现相等需要另议的情况。
循环用 left < right,停在一点。不能写成 left ≤ right 再 mid+1 / mid−1 两边都抛掉 mid:最小值可能正好在 mid,丢掉就错。没旋转时全程 nums[mid] < nums[right],right 一直收到 mid,最后停在 0,也正确。
用手走主例,对应三帧。left=0、right=4、mid=2,nums[2]=5 > nums[4]=2,断点在右,left 变成 3。第二刀 left=3、right=4、mid=3,nums[3]=1 < nums[4]=2,从 1 到 2 升序,right 收到 3。left == right == 3,最小值 nums[3]=1。
为什么比 right 不比 left左端永远落在大段或整段未旋的头上,和 mid 比分不清「mid 已经过断点」还是「根本没旋转」。右端一定落在小段或未旋的尾上,mid 比它大就说明中间必有下降。
六行 Go:跟着下降走
func findMin(nums []int) int {l, r := 0, len(nums)-1for l < r {m := l + (r-l)/2if nums[m] > nums[r] { l = m + 1 } else { r = m }}return nums[l]}
1l<r 保证区间至少两格才切。停下来时 l==r,那一格就是最小值。
2nums[m]>nums[r]:m 在大段,最小值严格在右边,l=m+1。主例第一刀 5>2,l=3。
3否则 m 到 r 升序,最小值在左边含 m,r=m 而不是 m−1。主例第二刀 1<2,r=3。
4返回 nums[l]。单元素数组循环不进,直接返回唯一的数。
总结
mid 比 right 大就去右,否则收左含 mid。主例两刀停在 1。
- 最小值是唯一下降处。主例 5 后面接到 1,1 就是答案。
- 循环必须留下 mid 的一种分支:r=m。两边都抛掉 mid 会在最小值正好是 mid 时出错。
- 和 LC33 共用「一半有序」,这里没有 target,只跟着断点走。