题目描述
一个升序数组在某点旋转过(如 [0,1,2,4,5,6,7] 转成 [4,5,6,7,0,1,2]),找出其中的最小值。
思路解析
挨个比当然能找到最小,可那是 O(n),白白浪费了「旋转前本来有序」。题目把有序明牌摆这了,得用二分把它砍到 O(log n)。
转折:最小值就是旋转点。怎么二分定位它?拿 nums[mid] 和右端点 nums[r] 比——这是不变量:若 nums[mid] > nums[r],说明从 mid 到 r 中间有断崖,最小值在 mid 右边(l=mid+1);若 nums[mid] < nums[r],说明 mid 到 r 已经是连续升序,最小值在 mid 或它左边(r=mid,别丢 mid)。为什么不和 nums[l] 比?因为左半整段可能都比最小值大,比左端判不出方向。
准备 · 闭区间 [0,6]:搜索范围是整个数组,l=0、r=6。比较的「锚」永远是当前的右端 nums[r]。
第 1 轮 · 取 mid:中点 mid=3,nums[3] = 7。和右端点 nums[6]=2 比。
第 1 轮 · 判断(负例分支):7 > 2,说明从 mid 走到 r 中间一定掉过崖(旋转点在右半),最小值在 mid 右边,且 mid 本身肯定不是最小。
第 1 轮 · 收缩:l 跳到 mid+1=4,左半 [4,5,6,7](灰)整段丢掉——这半里没有最小值。范围缩到 [4, 6]。
第 2 轮 · 取 mid:范围 [4,6],新中点 mid=5,nums[5] = 1。继续和右端 nums[6]=2 比。
第 2 轮 · 判断:1 < 2,说明从 mid 到 r 是连续升序(没掉崖),那这段最小的就在 mid 这头。最小值在 mid 或它左边,mid 可能就是答案。
第 2 轮 · 收缩:r 收到 mid=5(不是 mid−1,因为 mid 自己可能就是最小)。下标 6 丢掉,范围 [4, 5]。
第 3 轮 · 取 mid:范围 [4,5],mid=4,nums[4] = 0。和右端 nums[5]=1 比。
第 3 轮 · 收缩 → l==r:0 < 1,最小值在 mid 或其左,r 收到 4。现在 l == r == 4,区间只剩一个,循环结束。
答案:l 和 r 撞在下标 4,nums[4] = 0 就是最小值。每轮砍一半,O(log n) 搞定。
旋转找最小,记死一句:和右端点比。大于则断崖(最小值)在右,否则在左含 mid。和 nums[l] 比会判错,因为左半可能整段偏大。
参考代码(Python)
1def findMin(self, nums):
2 l, r = 0, len(nums) - 1 # 闭区间 [l, r]
3 while l < r: # 注意是 < 不是 <=
4 mid = l + (r - l) // 2
5 if nums[mid] > nums[r]: # 和右端比,断崖在右
6 l = mid + 1 # 最小值在 mid 右边
7 else: # nums[mid] < nums[r],已升序
8 r = mid # 最小值在 mid 或其左,别丢 mid
9 return nums[l] # l==r 时即最小值复杂度分析
- 时间复杂度:O(log n) —— 每轮把区间砍掉一半
- 空间复杂度:O(1) —— 只用 l、r、mid 三个变量
套路模板
骨架记牢:while l < r、和 nums[r] 比、大于则 l=mid+1 否则 r=mid。LC154(含重复)、LC33(旋转找 target)都是它的近亲。
1l, r = 0, len(nums) - 1
2while l < r:
3 mid = l + (r - l) // 2
4 if nums[mid] > nums[r]:
5 l = mid + 1 # 最小在右
6 else:
7 r = mid # 最小在左(含 mid)
8return nums[l]易错点
- 错误写法:拿 nums[mid] 和 nums[l] 比 → 正确写法:和 nums[r] 比(左半可能整段都比最小值大,和左端比判不出最小值在哪边;只有和右端比才能稳定定位断崖)
- 错误写法:nums[mid] < nums[r] 时写 r = mid - 1 → 正确写法:r = mid(不减 1)(mid 自己可能就是最小值,减 1 会把它丢掉,结果偏大或越界)
- 错误写法:循环写成 while l <= r → 正确写法:while l < r(本题靠 l==r 收尾返回 nums[l];用 <= 且 r=mid 不减会在 l==r 时死循环)
以上文字与上方动画完全同一思路:先看动画建立直觉 → 读文字巩固 → 关掉页面自己默写一遍代码,能写出来才算真的掌握。