寻找旋转排序数组中的最小值 II 图解题解
这道题到底在问什么
- 输入
- nums = [4,0,0,1,1,1,1,2,3,3,3,3,4]
- 输出
- 0(被转到中间那段的开头)
最优解:为什么这么做
一句话答案:LeetCode 154 寻找旋转排序数组中的最小值 II:在旋转过又带重复值的升序数组里找最小值,用二分——中点比右端大就往右收、小了往左收、相等时右端退一格,它有中点这个替身、丢了不亏,平均 O(log n)、最坏 O(n)。
旋转又带重复的数组里,要挖出哪个数
给一个升序数组,从某处整体转一下、后半段挪到前面接上,可能含重复数字,得到 nums,找里面最小的那个。升序被掰断再接起来,断裂那道『断崖』下面就是最小值。题面 nums=[4,0,0,1,1,1,1,2,3,3,3,3,4],最小值 0 被转到了中间那段的开头。
从头扫一遍也能找到,可惜白扫大半截
最直接是从头比到尾、记住最小数,n 个数扫 n 次,时间 O(n)。可这数组大半截还有序,挨个看把二分能省的一半也翻了遍,数到几万个就没必要。
盯着中点和右端比,怎么就知道最小值躲哪半
把中点 nums[mid] 和右端 nums[r] 比。nums[mid] 大于 nums[r],中点还在前半那段大数里,最小值在它右边;nums[mid] 小于 nums[r],中点已落进后半小数段,最小值在它左边、或就是它。
为什么盯右端不盯左端?和右端比三种大小方向干净;换成和左端比,大数段压在左边,方向反而容易判反。
相等时把右端退一格,为什么不会把最小值丢掉
两个指针 l、r 圈住还没排除的范围,起初 l=0、r=n-1。每轮取 mid=(l+r)//2 和 nums[r] 比:nums[mid] 大于 nums[r],最小值在右半,l 跳到 mid+1;nums[mid] 小于 nums[r],最小值在左半含中点,r 收到 mid(中点可能正是答案不能丢)。缩到 l、r 撞上,那格就是最小值。
麻烦全在相等:nums[mid] 等于 nums[r] 时,最小值在左在右都可能,方向判不出来。这时只把右端退一格(r-=1),凭什么不怕丢答案?因为右端 nums[r] 有中点 nums[mid] 这个一样的替身,中点还在 [l, r-1] 里。就算最小值恰好在 nums[r] 这格,答案要的是那个数值而非下标,丢掉它、同样大的中点还在,一分没少。绝不能跳半边:写成 nums[mid] 大于等于 nums[r] 就 l=mid+1,碰上 [1,0,1,1,1] 会跨过真正的 0。
题面这串数逐格收窄,l 和 r 一步步逼上去
起点 l=0、r=12。mid=6,nums[6]=1 小于 nums[12]=4,r 收到 6。r=6 时 mid=3,nums[3]=1 等于 nums[6]=1,右端退一格到 5。r=5 时 mid=2,nums[2]=0 小于 nums[5]=1,r 收到 2。r=2 时 mid=1,nums[1]=0 等于 nums[2]=0,右端退到 1。r=1 时 mid=0,nums[0]=4 大于 nums[1]=0,l 跳到 1。此刻 l、r 同停在 1,返回 nums[1]=0。
重复太多时,二分会退化成一格一格挪
正常每轮砍一半,时间 O(log n)、空间 O(1)。但相等分支每次只让 r 退一格,遇上 [2,2,2,2] 全重复,每轮才缩一格,最坏退化成 O(n)。
易错点也绕着相等打转:图快跳半边不写 r-=1,答案会丢在跳过的那半;比较对象错拿左端 nums[l],方向就反;收缩得写 r=mid 而非 mid-1,中点自己可能就是最小值,退过头会漏掉它。
▶ 动画逐步走查(共 22 步)——想跟着动画一帧帧对照就展开
- 3记住比较对象始终是「中点 vs 右端」:大于就往右走,小于就往左收,相等就把右端退一格。下面每一帧都在套这三条。
- 4当前还要搜的范围是下标 0 到 12(灰色格子已排除)。最小值一定在这段里,取中点把它再砍一半。
- 5区间 [0, 12] 的中点落在下标 6(值 1)。接下来拿它和「右端」nums[12]=4 比,判断最小值在哪一侧。
- 6拿中点和右端比:1 < 4,中点比右端小 → 中点已落到后面那段小数里,最小值在它左边或就是它本身。
- 7因为中点比右端小,最小值在中点左边或就是中点本身,所以右指针收到 mid=6(保留中点,别丢掉它)。范围再砍一半。
- 8当前还要搜的范围是下标 0 到 6(灰色格子已排除)。最小值一定在这段里,取中点把它再砍一半。
- 9区间 [0, 6] 的中点落在下标 3(值 1)。接下来拿它和「右端」nums[6]=1 比,判断最小值在哪一侧。
- 10拿中点和右端比:1 = 1,两值相等分不出最小值在哪边。但右端这一格(标红)和中点一样大,最坏它也只是个替身,丢掉它不会丢掉答案。
- 11把右指针退一格到 5,安全地排除掉那个重复的右端。区间缩小一点,继续二分。
- 12当前还要搜的范围是下标 0 到 5(灰色格子已排除)。最小值一定在这段里,取中点把它再砍一半。
- 13区间 [0, 5] 的中点落在下标 2(值 0)。接下来拿它和「右端」nums[5]=1 比,判断最小值在哪一侧。
- 14拿中点和右端比:0 < 1,中点比右端小 → 中点已落到后面那段小数里,最小值在它左边或就是它本身。
- 15因为中点比右端小,最小值在中点左边或就是中点本身,所以右指针收到 mid=2(保留中点,别丢掉它)。范围再砍一半。
- 16当前还要搜的范围是下标 0 到 2(灰色格子已排除)。最小值一定在这段里,取中点把它再砍一半。
- 17区间 [0, 2] 的中点落在下标 1(值 0)。接下来拿它和「右端」nums[2]=0 比,判断最小值在哪一侧。
- 18拿中点和右端比:0 = 0,两值相等分不出最小值在哪边。但右端这一格(标红)和中点一样大,最坏它也只是个替身,丢掉它不会丢掉答案。
- 19把右指针退一格到 1,安全地排除掉那个重复的右端。区间缩小一点,继续二分。
- 20当前还要搜的范围是下标 0 到 1(灰色格子已排除)。最小值一定在这段里,取中点把它再砍一半。
- 21区间 [0, 1] 的中点落在下标 0(值 4)。接下来拿它和「右端」nums[1]=0 比,判断最小值在哪一侧。
- 22拿中点和右端比:4 > 0,中点比右端大 → 中点还在前面那段大数里,最小值在它右边。
- 23因为中点比右端大,中点及它左边都不可能是最小值,把左指针跳到 mid+1=1。搜索范围砍掉一半。
- 24左右指针撞在下标 1,区间只剩这一格(高亮),它的值 0 就是整个数组的最小值。
⚠️ 容易写错的地方
✗ 错:拿 nums[mid] 和「左端 nums[l]」比
✓ 对:始终和「右端 nums[r]」比
和左端比时,旋转后的大数段在左边,判断方向容易反;和右端比时三种情况干净利落
✗ 错:相等时直接跳过半边
✓ 对:相等时只让 r 退一格(r -= 1)
nums[mid]==nums[r] 时最小值可能在任意一侧,跳半边会把答案丢掉;只退一格才安全
✗ 错:走 nums[mid] >= nums[r] 就 l=mid+1
✓ 对:把相等单独拎出来 r -= 1
相等时若 l=mid+1,可能跨过最小值(如 [1,0,1,1,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]: # 中点比右端大
l = mid + 1 # 最小值在右半
elif nums[mid] < nums[r]: # 中点比右端小
r = mid # 最小值在左半(含中点)
else: # 相等,分不清
r -= 1 # 右端是替身,退一格
return nums[l]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 if (nums[mid] < nums[r]) r = mid;
else r -= 1;
}
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 if (nums[mid] < nums[r]) r = mid;
else r -= 1;
}
return nums[l];
}复杂度
时间
O(log n)(平均)
每轮把区间砍一半;但全是重复值时(如 [2,2,2,2])相等分支每次只退一格,最坏退化成 O(n)
空间
O(1)
只用 l、r、mid 三个下标变量,不开额外数组
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 寻找旋转排序数组中的最小值 II 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
它和『寻找旋转排序数组最小值 I』(无重复)到底差在哪?+
差在有没有相等这种情况。I 里元素不重复,nums[mid] 和 nums[r] 只会大于或小于,方向永远判得清,稳定 O(log n)。II 多了 nums[mid] 等于 nums[r] 这一支,此时最小值在左在右都可能,只能保守地把右端退一格,遇到全是重复值的数组最坏退化到 O(n)。代码上就多出一个 else 分支写 r-=1,思路骨架和 I 完全一样。
为什么比较对象一定是 nums[r],换成 nums[l] 不行吗?+
和右端比,三种大小对应的方向特别干净:大于就是中点还在前段大数、最小值在右;小于就是中点已进后段小数、最小值在左;等于就退右端。若改和左端 nums[l] 比,旋转后的大数段正好压在左边,nums[mid] 大于 nums[l] 时最小值可能在左也可能在右,得再分情况讨论,代码一下子啰嗦且容易判反。始终盯右端是这道题最省心的锚。
如果数组根本没被旋转(本来就是升序)会不会出错?+
不会,逻辑照样自洽。没旋转时整段升序,每轮 nums[mid] 要么小于 nums[r]、要么和 nums[r] 相等,r 只会不断往左收,绝不会走 l=mid+1 那支,最终停在下标 0,返回 nums[0]——正好是升序数组本来的最小值。相等退一格这步在这种全等或部分等的数组上也只是慢一点,不会给错答案。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 寻找旋转排序数组中的最小值 II 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。