题目描述
思路解析
一句话答案:LeetCode 153 寻找旋转排序数组中的最小值:旋转后是两段升序,二分时拿 mid 跟最右端比,比它大最小值在右半 l=mid+1,否则在左半含 mid 处 r=mid,夹到 l==r。时间 O(log n)、空间 O(1)。
旋转过的升序数组,最小值藏在哪
给一个原本升序、元素互不相同的数组,前面一段整体挪到末尾(这叫旋转),问最小的数是几。题面 nums=[4,5,6,7,0,1,2] 就是升序 [0,1,2,4,5,6,7] 把 [0,1,2] 挪到末尾,最小值 0 在下标 4。题目卡死时间 O(log n),逼你别硬扫一遍。
扫一遍就能找到,为什么还要费劲二分
笨办法是从头扫一遍、把见过的最小值记住,n 次比较、O(n)(n 是数组长度)。可这没用上『数组本来有序』,把它当乱数处理了。题目要 O(log n),每步得砍掉一大片、不能逐格挪,这就得靠二分查找;只是数组被旋转过、不再整体递增,普通二分找 target 那套不能照抄。
转过一圈的数组,凭什么还能二分
旋转后不再整体升序,但结构规整:从断裂处劈成两段,每段各自升序,且前段每个数都比后段大。题面里 [4,5,6,7] 是偏大前段、[0,1,2] 是偏小后段,交界那个 0 就是最小值。随便挑一个位置,它落在哪段都判得出来——拿它跟一个固定参照比大小即可。归属可判、答案又卡在交界,就能用二分把范围往交界处夹。
中点为什么跟最右端比,而不是最左端
二分维护左右边界 l、r,每轮取中点 mid(当前范围正中间那个下标,mid=(l+r)//2)。命门是拿 nums[mid] 跟谁比:这里比最右端 nums[r],不比最左端——数组转过时最右端始终在偏小的后段,当参照最稳。nums[mid]>nums[r] 说明 mid 在偏大前段,最小值在右边,左半连 mid 丢掉,l=mid+1;否则 mid 在偏小后段,最小值就是 mid 或在它左边,r=mid 把 mid 留着(它可能正是最小值)。比最左端则参照不稳、分不清 mid 在哪段。
把 [4,5,6,7,0,1,2] 从两头往中间夹
起手 l=0、r=6。第 1 轮 mid=3,nums[3]=7 大于最右端 nums[6]=2,mid 在前段,l=mid+1=4,缩到 [4,6]。第 2 轮 mid=5,nums[5]=1 不大于 nums[6]=2,mid 在后段,r=mid=5、留着 mid,缩到 [4,5]。第 3 轮 mid=4,nums[4]=0 不大于 nums[5]=1,r=mid=4,缩到 [4,4]。l 与 r 在下标 4 撞上,返回 nums[4]=0。
开闭区间配错,末格会一直空转
循环条件得用 while l<r:收缩靠 r=mid 保留了 mid,写成 l<=r 会在 l==r 时还进循环、取 mid=l 令 r=mid=l,边界一步不动、末格空转成死循环。偏小侧也只能 r=mid,写成 r=mid-1 会漏掉可能正是最小值的 mid、把答案跳过去。mid 则用 l+(r-l)//2 代替 (l+r)//2 更稳,数组极大时 l+r 相加不会顶破整数上限。整个过程每轮砍掉一半,最多约 log₂n 轮夹到一格,时间 O(log n);只用 l、r、mid 三个下标,空间 O(1)。
▶ 动画逐步走查(文字版)——想跟着上方动画一帧帧对照就展开
记住比较对象是「最右端 nums[r]」,不是 target。比右端大 → 最小值在右半;否则在左半(含自己)。每轮范围减半。
开始:左边界 l=0,右边界 r=11,整个数组都是搜索范围。目标是用二分把范围一点点夹小到只剩一格。
先看清结构:这个数组是「两段各自升序」拼起来的,前段都大、后段都小。它们的交界处(高亮的下标 6,值 0)就是答案,二分要做的就是稳稳夹到这里。
第 1 轮:在范围 [0, 11] 里取正中间 mid=5。下面这一格就是当前要考察的中点。
看中点这一格的值 nums[5]=11。接下来的判断只看一件事:它和最右端 nums[11]=5 谁大。
比较:nums[5]=11 大于 最右端 nums[11]=5。说明 mid 还在大的前段,最小值在它右边。
把左半(含 mid 这格)整段灰掉排除,l 跳到 6。范围一下砍掉一半,缩到 [6, 11]。
第 2 轮:在范围 [6, 11] 里取正中间 mid=8。下面这一格就是当前要考察的中点。
看中点这一格的值 nums[8]=2。接下来的判断只看一件事:它和最右端 nums[11]=5 谁大。
比较:nums[8]=2 不大于 最右端 nums[11]=5。说明 mid 已在小的后段,最小值在它这边或左边。
mid 右边那段整段灰掉排除,r 收到 8。注意 mid 自己留着——它可能正好是最小值,不能丢。范围缩到 [6, 8]。
第 3 轮:在范围 [6, 8] 里取正中间 mid=7。下面这一格就是当前要考察的中点。
看中点这一格的值 nums[7]=1。接下来的判断只看一件事:它和最右端 nums[8]=2 谁大。
比较:nums[7]=1 不大于 最右端 nums[8]=2。说明 mid 已在小的后段,最小值在它这边或左边。
mid 右边那段整段灰掉排除,r 收到 7。注意 mid 自己留着——它可能正好是最小值,不能丢。范围缩到 [6, 7]。
第 4 轮:在范围 [6, 7] 里取正中间 mid=6。下面这一格就是当前要考察的中点。
看中点这一格的值 nums[6]=0。接下来的判断只看一件事:它和最右端 nums[7]=1 谁大。
比较:nums[6]=0 不大于 最右端 nums[7]=1。说明 mid 已在小的后段,最小值在它这边或左边。
mid 右边那段整段灰掉排除,r 收到 6。注意 mid 自己留着——它可能正好是最小值,不能丢。范围缩到 [6, 6]。
左右边界在下标 6 撞到一起,范围只剩一格——高亮的 nums[6]=0 就是整个数组的最小值。
验证一下:高亮这格 nums[6]=0,左邻是 11、右邻是 1,都比它大,它就是整个旋转数组的最小值。二分只比了 4 次就锁定了它。
三个高频追问:为何比右端、无旋转的边界、以及循环条件 l<r 的道理。
参考代码
def findMin(nums): l, r = 0, len(nums) - 1 while l < r: # 范围多于一格就继续二分 mid = (l + r) // 2 if nums[mid] > nums[r]: # mid 在偏大的前段 l = mid + 1 # 最小值在右半,丢掉左半+mid else: # mid 在偏小的后段 r = mid # 最小值在 mid 这边或左边,保留 mid return nums[l] # l==r,落在最小值上复杂度
- 时间:O(log n),每轮比较都砍掉一半范围,最多比较约 log₂n 次
- 空间:O(1),只用 l、r、mid 三个下标变量,不开额外数组
易错点
面试追问把动画讲成自己的话
追问为什么和 nums[r] 比,而不是和 nums[l] 比?
追问如果数组根本没旋转(已经是升序)呢?
追问循环条件为什么是 while l < r 而不是 l <= r?
这道题到这就讲完了。动画和文字是同一套思路——别光看,关掉页面自己默写一遍。然后顺着主线继续:
搜索旋转排序数组
LeetCode 33 · 中等 · 沿着 二分查找 继续往下推进
把这道题真正学会,再走
图解算法年卡 ¥99 /年
- ✓本题每步动画的吴师兄语音讲解(全站陆续覆盖)
- ✓小欧带 8 步通关训练——追问到不看答案也能写对、能 30 秒讲给面试官听
- ✓学习报告,记录每道题的掌握程度
76k+ GitHub Star · 吴师兄开源图解算法,几十万开发者在看的算法讲解
想成体系刷透这类套路?去图解算法专题