题目描述
思路解析
一句话答案: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,中点自己可能就是最小值,退过头会漏掉它。
▶ 动画逐步走查(文字版)——想跟着上方动画一帧帧对照就展开
记住比较对象始终是「中点 vs 右端」:大于就往右走,小于就往左收,相等就把右端退一格。下面每一帧都在套这三条。
当前还要搜的范围是下标 0 到 12(灰色格子已排除)。最小值一定在这段里,取中点把它再砍一半。
区间 [0, 12] 的中点落在下标 6(值 1)。接下来拿它和「右端」nums[12]=4 比,判断最小值在哪一侧。
拿中点和右端比:1 < 4,中点比右端小 → 中点已落到后面那段小数里,最小值在它左边或就是它本身。
因为中点比右端小,最小值在中点左边或就是中点本身,所以右指针收到 mid=6(保留中点,别丢掉它)。范围再砍一半。
当前还要搜的范围是下标 0 到 6(灰色格子已排除)。最小值一定在这段里,取中点把它再砍一半。
区间 [0, 6] 的中点落在下标 3(值 1)。接下来拿它和「右端」nums[6]=1 比,判断最小值在哪一侧。
拿中点和右端比:1 = 1,两值相等分不出最小值在哪边。但右端这一格(标红)和中点一样大,最坏它也只是个替身,丢掉它不会丢掉答案。
把右指针退一格到 5,安全地排除掉那个重复的右端。区间缩小一点,继续二分。
当前还要搜的范围是下标 0 到 5(灰色格子已排除)。最小值一定在这段里,取中点把它再砍一半。
区间 [0, 5] 的中点落在下标 2(值 0)。接下来拿它和「右端」nums[5]=1 比,判断最小值在哪一侧。
拿中点和右端比:0 < 1,中点比右端小 → 中点已落到后面那段小数里,最小值在它左边或就是它本身。
因为中点比右端小,最小值在中点左边或就是中点本身,所以右指针收到 mid=2(保留中点,别丢掉它)。范围再砍一半。
当前还要搜的范围是下标 0 到 2(灰色格子已排除)。最小值一定在这段里,取中点把它再砍一半。
区间 [0, 2] 的中点落在下标 1(值 0)。接下来拿它和「右端」nums[2]=0 比,判断最小值在哪一侧。
拿中点和右端比:0 = 0,两值相等分不出最小值在哪边。但右端这一格(标红)和中点一样大,最坏它也只是个替身,丢掉它不会丢掉答案。
把右指针退一格到 1,安全地排除掉那个重复的右端。区间缩小一点,继续二分。
当前还要搜的范围是下标 0 到 1(灰色格子已排除)。最小值一定在这段里,取中点把它再砍一半。
区间 [0, 1] 的中点落在下标 0(值 4)。接下来拿它和「右端」nums[1]=0 比,判断最小值在哪一侧。
拿中点和右端比:4 > 0,中点比右端大 → 中点还在前面那段大数里,最小值在它右边。
因为中点比右端大,中点及它左边都不可能是最小值,把左指针跳到 mid+1=1。搜索范围砍掉一半。
左右指针撞在下标 1,区间只剩这一格(高亮),它的值 0 就是整个数组的最小值。
三个高频追问:和 I 的区别、为何比右端、以及未旋转的边界。
参考代码
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]复杂度
- 时间:O(log n)(平均),每轮把区间砍一半;但全是重复值时(如 [2,2,2,2])相等分支每次只退一格,最坏退化成 O(n)
- 空间:O(1),只用 l、r、mid 三个下标变量,不开额外数组
易错点
面试追问把动画讲成自己的话
追问它和「寻找旋转排序数组最小值 I」(无重复)有什么区别?
追问为什么比较对象是 nums[r] 而不是 nums[l]?
追问如果数组完全没旋转(本来就升序)会怎样?
这道题到这就讲完了。动画和文字是同一套思路——别光看,关掉页面自己默写一遍。再去主线里挑下一道练手。
把这道题真正学会,再走
图解算法年卡 ¥99 /年
- ✓本题每步动画的吴师兄语音讲解(全站陆续覆盖)
- ✓小欧带 8 步通关训练——追问到不看答案也能写对、能 30 秒讲给面试官听
- ✓学习报告,记录每道题的掌握程度
76k+ GitHub Star · 吴师兄开源图解算法,几十万开发者在看的算法讲解
想成体系刷透这类套路?去图解算法专题