寻找旋转排序数组中的最小值 图解题解
旋转数组总有一半是完整升序——靠 mid 和右端点那一比,每步排掉没有最小值的半边。
旋转数组像一把尺子从中间折过一次:整体乱了,但两段各自还是顺序的。找最小值就是找「折痕左边那个断崖」。把 nums[mid] 和右端点比:mid 比 r 大,说明从 mid 到 r 中间有掉崖,最小值在右半;mid 比 r 小,说明 mid 到 r 是完整升序,最小值在 mid 或其左边。每次砍掉没有断崖的一半,O(log n) 找到折痕。
这道题到底在问什么
- 输入
- nums = [4,5,6,7,0,1,2]
- 输出
- 0(旋转点处那个最小值)
最优解:为什么这么做
一句话答案: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)。
▶ 动画逐步走查(共 21 步)——想跟着动画一帧帧对照就展开
- 3记住比较对象是「最右端 nums[r]」,不是 target。比右端大 → 最小值在右半;否则在左半(含自己)。每轮范围减半。
- 4开始:左边界 l=0,右边界 r=11,整个数组都是搜索范围。目标是用二分把范围一点点夹小到只剩一格。
- 5先看清结构:这个数组是「两段各自升序」拼起来的,前段都大、后段都小。它们的交界处(高亮的下标 6,值 0)就是答案,二分要做的就是稳稳夹到这里。
- 6第 1 轮:在范围 [0, 11] 里取正中间 mid=5。下面这一格就是当前要考察的中点。
- 7看中点这一格的值 nums[5]=11。接下来的判断只看一件事:它和最右端 nums[11]=5 谁大。
- 8比较:nums[5]=11 大于 最右端 nums[11]=5。说明 mid 还在大的前段,最小值在它右边。
- 9把左半(含 mid 这格)整段灰掉排除,l 跳到 6。范围一下砍掉一半,缩到 [6, 11]。
- 10第 2 轮:在范围 [6, 11] 里取正中间 mid=8。下面这一格就是当前要考察的中点。
- 11看中点这一格的值 nums[8]=2。接下来的判断只看一件事:它和最右端 nums[11]=5 谁大。
- 12比较:nums[8]=2 不大于 最右端 nums[11]=5。说明 mid 已在小的后段,最小值在它这边或左边。
- 13mid 右边那段整段灰掉排除,r 收到 8。注意 mid 自己留着——它可能正好是最小值,不能丢。范围缩到 [6, 8]。
- 14第 3 轮:在范围 [6, 8] 里取正中间 mid=7。下面这一格就是当前要考察的中点。
- 15看中点这一格的值 nums[7]=1。接下来的判断只看一件事:它和最右端 nums[8]=2 谁大。
- 16比较:nums[7]=1 不大于 最右端 nums[8]=2。说明 mid 已在小的后段,最小值在它这边或左边。
- 17mid 右边那段整段灰掉排除,r 收到 7。注意 mid 自己留着——它可能正好是最小值,不能丢。范围缩到 [6, 7]。
- 18第 4 轮:在范围 [6, 7] 里取正中间 mid=6。下面这一格就是当前要考察的中点。
- 19看中点这一格的值 nums[6]=0。接下来的判断只看一件事:它和最右端 nums[7]=1 谁大。
- 20比较:nums[6]=0 不大于 最右端 nums[7]=1。说明 mid 已在小的后段,最小值在它这边或左边。
- 21mid 右边那段整段灰掉排除,r 收到 6。注意 mid 自己留着——它可能正好是最小值,不能丢。范围缩到 [6, 6]。
- 22左右边界在下标 6 撞到一起,范围只剩一格——高亮的 nums[6]=0 就是整个数组的最小值。
- 23验证一下:高亮这格 nums[6]=0,左邻是 11、右邻是 1,都比它大,它就是整个旋转数组的最小值。二分只比了 4 次就锁定了它。
⚠️ 容易写错的地方
✗ 错:拿 nums[mid] 和 nums[l] 比
✓ 对:拿 nums[mid] 和 nums[r] 比
和左端比时,若 mid 落在前段会出现 nums[mid]≥nums[l] 但最小值仍在右边的歧义;和右端比才能稳定判断 mid 在哪一段
✗ 错:nums[mid] > nums[r] 后写成 r = mid
✓ 对:应写 l = mid + 1
mid 比右端还大,它一定在偏大的前段,绝不可能是最小值,必须连 mid 一起排除
✗ 错:nums[mid] ≤ nums[r] 后写成 r = mid - 1
✓ 对:应写 r = mid
mid 自己可能就是最小值,写 mid-1 会把答案丢掉
完整代码(Python / C++ / Java)
Python
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,落在最小值上C++
int findMin(vector<int>& nums){
int l = 0, r = nums.size() - 1;
while (l < r) {
int mid = (l + r) / 2;
if (nums[mid] > nums[r]) l = mid + 1;
else r = mid;
}
return nums[l];
}Java
public int findMin(int[] nums) {
int l = 0, r = nums.length - 1;
while (l < r) {
int mid = (l + r) / 2;
if (nums[mid] > nums[r]) l = mid + 1;
else r = mid;
}
return nums[l];
}复杂度
时间
O(log n)
每轮比较都砍掉一半范围,最多比较约 log₂n 次
空间
O(1)
只用 l、r、mid 三个下标变量,不开额外数组
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 寻找旋转排序数组中的最小值 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么拿 mid 和最右端比,而不是最左端?+
只要数组转过,最右端 nums[r] 就一定落在偏小的后段,是个稳定参照:nums[mid] 比它大,mid 必在前段,最小值在右边;nums[mid] 比它小或相等,mid 已进后段,最小值就是 mid 或在更左。换成最左端就不稳——nums[l] 在前段时比后段每个数都大,nums[mid] 无论落哪段都可能比它小,判不出 mid 在哪一段,二分没法收缩。
数组没被旋转、本来就升序,会不会出错?+
不会。若 nums 整体升序,比如 [0,1,2,4,5,6,7],每轮 nums[mid] 都不大于最右端 nums[r],判定一路走 r=mid,把范围一直往左夹,最后停在下标 0,返回 nums[0] 这个最小值。因为收缩规则里『小于等于』把相等也归进后段,最右端本身就是升序数组的最大值、不会误导,边界自然收在最左。
这题元素不重复,若允许重复(LeetCode 154)会怎样?+
重复会让最右端这个参照失灵。比如 [3,3,1,3],nums[mid] 和 nums[r] 相等时,没法判断 mid 到底在哪一段,最小值可能在左也可能在右。154 的补丁是相等时不砍一半、只把 r 减一格 r=r-1 稳妥缩范围,最坏退化成 O(n)。本题无重复就没这顾虑,每轮都能干净砍半。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 寻找旋转排序数组中的最小值 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。