搜索旋转排序数组 II 图解题解
这道题到底在问什么
- 输入
- nums = [1,1,1,0,1], target = 0
- 输出
- true(0 在下标 3)
最优解:为什么这么做
一句话答案:LeetCode 81 搜索旋转排序数组 II:在旋转过又带重复值的升序数组里判断 target 在不在,用旋转二分找它,但重复值让「哪半有序」失灵——三数相等时左右各缩一格去重,平均 O(log n)、最坏退化 O(n)。
数组被旋转过又带重复,只判 target 在不在
nums 本是升序,从某处切开对调,比如 [0,1,1,1] 变 [1,1,1,0],还可能有重复数字。给定 target,在就返回 true、否则 false,只判在不在。题面 nums=[1,1,1,0,1]、target=0 为 true。
扫一遍能过,可为什么不甘心停在 O(n)
把 target 和每个数挨个比,n 个数最坏比满 n 次,是 O(n)。数据小也能过,但数组是升序旋转来的、自带秩序没用上;升序本可二分 O(log n) 定位,题目旋转又掺重复,就是逼你捡回二分。
凭什么还能二分,重复又是怎么搅局的
旋转后数组不再整体有序,却是两段各自升序的拼接。以中点 mid 为界,nums[l]≤nums[mid] 就说明左半升序,否则拐点在左半、右半才升序;锁定有序那半看 target 在不在它的范围里,在就进这半、不在去另一半,一样砍掉一半。
麻烦全在重复:nums[l]=nums[mid] 时左半可能有序也可能不是,光看分不清;尤其三者全相等,大小比较彻底失去区分力,哪半有序都判不了。
三个数一样时,指针到底该往哪挪
每轮取中点 mid=(l+r)//2,nums[mid]==target 就返回 true。没中分三支:三者 nums[l]、nums[mid]、nums[r] 相等时判不出哪半有序,只能 l 加一、r 减一各缩一格,最坏退化就埋在这;nums[l]≤nums[mid] 时左半有序,nums[l]≤target<nums[mid] 收左 r=mid-1、否则 l=mid+1;否则右半有序,nums[mid]<target≤nums[r] 收右 l=mid+1、否则 r=mid-1。开闭对齐有序方向,写反等号就把 target 推错半边。
拿 [1,1,1,0,1] 找 0,三轮各发生什么
起手 l=0、r=4。第一轮 mid=(0+4)//2=2,nums[2]=1 不是 0;nums[0]、nums[2]、nums[4] 全是 1,三者相等,各缩一格,l 到 1、r 到 3。
第二轮 l=1、r=3,mid=2,nums[2]=1 不是 0;nums[l]=nums[mid]=1 但 nums[r]=nums[3]=0,不触发三者相等,nums[l]≤nums[mid] 判左半有序,1≤0<1 不成立故 target 靠右,l=mid+1=3。第三轮 l=r=3,mid=3,nums[3]=0 命中返回 true。首轮靠缩一格跨过重复区,后两轮才是常规二分。
少了缩一格这一支,重复数组会怎么判错
砍半时是 O(log n)、空间 O(1);但 [1,1,1,1,1] 找 0 这种几乎全相等的数组,每轮都撞三者相等只能缩一格,退化成 O(n)。
照搬 33 题漏掉 nums[l]==nums[mid]==nums[r] 这支,重复数组哪半有序会判错、区间收不动死循环。相等时只动一边、l++ 忘了 r--,某些分布下区间收不干净会漏看 target,两端得同时缩。开闭的等号也别写反,左半该 <、右半该 ≤,站错边 target 就判到另一半。
▶ 动画逐步走查(共 21 步)——想跟着动画一帧帧对照就展开
- 3三件事:先看中点中没中;没中就找出有序的那一半;有序半里用大小关系决定往左还是往右缩。三者相等是特例,左右各缩一格。
- 4左指针 l 指向下标 0,右指针 r 指向下标 13,搜索区间是整个数组。目标值 target = 0。
- 5在区间 [0, 13] 里取中点 mid = (0+13)/2 = 6,这一格的值是 1。先拿它和目标 0 比。
- 6nums[l]、nums[mid]、nums[r] 全是 1,看不出哪半段是有序的。这是重复值的特例:把 l 加一、r 减一,各缩一格再继续。
- 7在区间 [1, 12] 里取中点 mid = (1+12)/2 = 6,这一格的值是 1。先拿它和目标 0 比。
- 8nums[l]、nums[mid]、nums[r] 全是 1,看不出哪半段是有序的。这是重复值的特例:把 l 加一、r 减一,各缩一格再继续。
- 9在区间 [2, 11] 里取中点 mid = (2+11)/2 = 6,这一格的值是 1。先拿它和目标 0 比。
- 10nums[l]、nums[mid]、nums[r] 全是 1,看不出哪半段是有序的。这是重复值的特例:把 l 加一、r 减一,各缩一格再继续。
- 11在区间 [3, 10] 里取中点 mid = (3+10)/2 = 6,这一格的值是 1。先拿它和目标 0 比。
- 12nums[l]、nums[mid]、nums[r] 全是 1,看不出哪半段是有序的。这是重复值的特例:把 l 加一、r 减一,各缩一格再继续。
- 13在区间 [4, 9] 里取中点 mid = (4+9)/2 = 6,这一格的值是 1。先拿它和目标 0 比。
- 14nums[l]、nums[mid]、nums[r] 全是 1,看不出哪半段是有序的。这是重复值的特例:把 l 加一、r 减一,各缩一格再继续。
- 15在区间 [5, 8] 里取中点 mid = (5+8)/2 = 6,这一格的值是 1。先拿它和目标 0 比。
- 16nums[l]=1 ≤ nums[mid]=1,说明左半段 [5, 6] 是连续升序的。看 target=0 落不落在这段的范围 [1, 1) 里:不在里面,往右缩。
- 17target 不在左半那段里,那就只能在右边,把左指针推到 mid 右边:l = mid+1 = 7。变灰的部分被排除,下一轮在 [7, 8] 里找。
- 18在区间 [7, 8] 里取中点 mid = (7+8)/2 = 7,这一格的值是 2。先拿它和目标 0 比。
- 19nums[l]=2 ≤ nums[mid]=2,说明左半段 [7, 7] 是连续升序的。看 target=0 落不落在这段的范围 [2, 2) 里:不在里面,往右缩。
- 20target 不在左半那段里,那就只能在右边,把左指针推到 mid 右边:l = mid+1 = 8。变灰的部分被排除,下一轮在 [8, 8] 里找。
- 21在区间 [8, 8] 里取中点 mid = (8+8)/2 = 8,这一格的值是 0。先拿它和目标 0 比。
- 22nums[8] = 0,正好等于目标 0!目标存在,返回 true,搜索结束。
- 23整趟下来,target=0 在高亮的下标 8 处被找到,最终返回 true。
⚠️ 容易写错的地方
✗ 错:照搬无重复版,忘了处理三者相等
✓ 对:先判 nums[l]==nums[mid]==nums[r],相等就 l++、r--
有重复时三者相等会让「哪半有序」的判断失效,不缩一格会死循环或判错
✗ 错:判断 target 落区间时边界开闭写错
✓ 对:左半用 nums[l] ≤ target < nums[mid],右半用 nums[mid] < target ≤ nums[r]
开闭写反会把 target 漏判或错判到另一半,结果出错
✗ 错:只缩一边导致区间不收敛
✓ 对:三者相等时 l 和 r 同时各缩一格
只动一边在某些重复分布下区间收不干净,会漏掉答案
完整代码(Python / C++ / Java)
Python
def search(nums, target):
l, r = 0, len(nums) - 1
while l <= r:
mid = (l + r) // 2
if nums[mid] == target: # 命中
return True
if nums[l] == nums[mid] == nums[r]: # 三者相等:缩一格
l += 1; r -= 1
elif nums[l] <= nums[mid]: # 左半有序
if nums[l] <= target < nums[mid]:
r = mid - 1
else:
l = mid + 1
else: # 右半有序
if nums[mid] < target <= nums[r]:
l = mid + 1
else:
r = mid - 1
return FalseC++
bool search(vector<int>& nums, int target){
int l = 0, r = nums.size() - 1;
while (l <= r) {
int mid = (l + r) / 2;
if (nums[mid] == target) return true;
if (nums[l]==nums[mid] && nums[mid]==nums[r]) { l++; r--; }
else if (nums[l] <= nums[mid]) { // 左半有序
if (nums[l] <= target && target < nums[mid]) r = mid - 1;
else l = mid + 1;
} else { // 右半有序
if (nums[mid] < target && target <= nums[r]) l = mid + 1;
else r = mid - 1;
}
}
return false;
}Java
public boolean search(int[] nums, int target) {
int l = 0, r = nums.length - 1;
while (l <= r) {
int mid = (l + r) / 2;
if (nums[mid] == target) return true;
if (nums[l]==nums[mid] && nums[mid]==nums[r]) { l++; r--; }
else if (nums[l] <= nums[mid]) { // 左半有序
if (nums[l] <= target && target < nums[mid]) r = mid - 1;
else l = mid + 1;
} else { // 右半有序
if (nums[mid] < target && target <= nums[r]) l = mid + 1;
else r = mid - 1;
}
}
return false;
}复杂度
时间
平均 O(log n)
每轮把区间砍一半;但全是重复值时退化到 O(n),因为只能一格一格缩
空间
O(1)
只用 l、r、mid 三个下标,不开额外数组
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 搜索旋转排序数组 II 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
它和 33 题(无重复的旋转数组搜索)到底差在哪?+
主干完全一样:每轮取中点、判断哪半有序、再看 target 在不在有序那半的范围里、往对应方向砍半。差别只在多出一条分支:当 nums[l]、nums[mid]、nums[r] 三个数恰好全相等时,33 题里靠 nums[l] 与 nums[mid] 的大小关系判断哪半有序,这时彻底失效,只能保守地 l++、r-- 各缩一格。正是这一支,把最坏复杂度从稳定的 O(log n) 拖到了 O(n)。
最坏情况什么时候冒出来,复杂度到底是多少?+
当数组几乎全是同一个值、比如 [1,1,1,1,1] 里找 0 时最糟。每一轮 nums[l]、nums[mid]、nums[r] 都相等,永远进不了「哪半有序」的正常分支,只能一次缩掉两端各一格。n 个数就要缩大约 n/2 轮,整体退化成 O(n),和直接从头扫一遍没了差距。这也是为什么有重复版通常只标平均 O(log n)、并注明最坏 O(n),而不敢像 33 题那样承诺稳定的对数级。
为什么只返回存不存在,不像 33 题返回下标?+
因为有重复值,target 可能同时躺在好几个位置,返回其中某一个下标既不唯一、也没有特别的意义,你说返回第一个还是最后一个?题目干脆只问「在不在」。而且三者相等时各缩一格的做法,本就是把两端两个数当作「无信息」直接丢弃,过程里并不保证锁定到某个确定下标,只判存在反而和算法的能力刚好匹配。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 搜索旋转排序数组 II 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。