搜索旋转排序数组 图解题解
旋转数组整体无序,但从 mid 一切,总能认出有序的那半——target 在不在里面,一比就知道往哪走。
旋转数组从 mid 切两半,必有一半是完整升序——就像一根折弯的尺,两段各自仍标着连续刻度。认出哪半有序(比较两个端点即可),再看 target 落不落在那半的范围里:落进去就去那半,落不进就去另一半。每步砍一半,二分在「局部有序」里也能用。
这道题到底在问什么
- 输入
- nums=[4,5,6,7,0,1,2], target=0
- 输出
- 4 (nums[4] = 0)
最优解:为什么这么做
一句话答案:LeetCode 33 搜索旋转排序数组的解法是改良二分查找:mid 把区间切成两半后必有一半是完整有序的(nums[l] <= nums[mid] 则左半有序,否则右半有序),再看 target 是否落在有序半的值域内——在就收缩到有序半,不在就去另一半。每步稳定砍掉一半区间,时间 O(log n)、空间 O(1)。
搜索旋转排序数组在问什么
一个原本升序、无重复元素的数组,在某个位置被整体旋转过(比如 [0,1,2,4,5,6,7] 旋转成 [4,5,6,7,0,1,2]),给定 target,要求返回它的下标,不存在返回 -1,并且明确要求 O(log n)。这个复杂度要求本身就是提示:线性扫描被排除了,题目在逼你把二分查找改造到「不完全有序」的数组上。
普通二分为什么在旋转数组上失效
二分查找的根基是单调性:数组有序时,拿 nums[mid] 和 target 比一下大小,就知道目标只可能在左半或右半,方向唯一。旋转之后数组整体不再单调,中间藏着一个从最大值跌到最小值的断点,nums[mid] 比 target 大还是小不再指示方向——目标可能在任意一侧。直接套普通二分,会在错误的一半里越找越偏。
关键观察:mid 切两半,必有一半有序
救回二分的观察是:旋转数组只有一个断点,而 mid 把区间切成两半,断点只能落进其中一半——另一半必然是完整有序的。而且哪半有序当场可判:nums[l] <= nums[mid] 说明从 l 到 mid 一路没有跨过断点,左半 [l..mid] 有序;反之断点在左半,右半 [mid..r] 有序。
拿示例 [4,5,6,7,0,1,2] 找 target = 0 验证:首轮 mid 落在 7,nums[0] = 4 <= 7,左半 [4,5,6,7] 有序,0 不在 4 到 7 之间,于是放心奔右半——一步就丢掉了一半。
为什么查有序半的值域就能安全决策
有序半的价值在于它的值域是明确的闭合范围:左半有序时值域是 nums[l] 到 nums[mid],右半有序时是 nums[mid] 到 nums[r]。于是判断变成一道二选一:target 落在这个范围里,它若存在就只能在有序半,收缩过去;不在范围里,则只可能藏在另一半(那半虽然乱,但排除法保证方向正确)。
每一轮无论走哪个分支,区间都严格减半,这就是 O(log n) 的来源。二分不怕那一半内部乱,怕的是不知道往哪走——有序半的值域恰好提供了永远可靠的路标。
复杂度与边界符号怎么写才不错
时间 O(log n),最多 log₂n 轮;空间 O(1),只用 l、mid、r 三个下标。边界符号是这题的雷区:判左半有序要用 nums[l] <= nums[mid](含等号,兼容区间只剩一个元素的情形);判值域时左半用 nums[l] <= target < nums[mid]、右半用 nums[mid] < target <= nums[r]——mid 本身已经在循环开头和 target 比过、确认不相等,所以范围要把 mid 排除、把端点含进来。循环条件必须是 l <= r,写成 l < r 会在区间只剩一个元素时漏查、误报 -1。另外,若数组允许重复元素(LeetCode 81),nums[l] 等于 nums[mid] 时判不出哪半有序,只能逐格收缩,最坏退化到 O(n)——本题保证无重复,才有稳定的对数复杂度。
▶ 动画逐步走查(共 26 步)——想跟着动画一帧帧对照就展开
- 3核心口诀:mid 切两半 → 必有一半有序 → 看 target 在不在有序半范围 → 在就去有序半、不在去另一半。下面每帧都在套它。
- 5mid=5(值 0)。因为 nums[l]=14 > nums[mid]=0,左半被旋转断了,反而是右半 [5..11] 有序(0…11)。target 11 正好落在这个有序半的范围里,所以缩到右半。
- 6把左指针挪到 mid+1=6,灰掉 mid 及其左边整片,搜索区间收缩到 [6, 11]。
- 7mid=8(值 4)。因为 nums[l]=1 ≤ nums[mid]=4,左半 [6..8] 是有序的(1…4)。target 11 不在这个有序半的范围里,所以去另一半(右半)。
- 8把左指针挪到 mid+1=9,灰掉 mid 及其左边整片,搜索区间收缩到 [9, 11]。
- 9mid=10(值 9)。因为 nums[l]=6 ≤ nums[mid]=9,左半 [9..10] 是有序的(6…9)。target 11 不在这个有序半的范围里,所以去另一半(右半)。
- 10把左指针挪到 mid+1=11,灰掉 mid 及其左边整片,搜索区间收缩到 [11, 11]。
- 11mid=11,nums[11]=11 正好等于 target 11,命中!返回下标 11。
- 12命中的格子绿色高亮。每一步都把区间砍一半,4 步就锁定了答案下标 11,这就是 O(log n)。
- 14mid=5(值 0)。因为 nums[l]=14 > nums[mid]=0,左半被旋转断了,反而是右半 [5..11] 有序(0…11)。target 2 正好落在这个有序半的范围里,所以缩到右半。
- 15把左指针挪到 mid+1=6,灰掉 mid 及其左边整片,搜索区间收缩到 [6, 11]。
- 16mid=8(值 4)。因为 nums[l]=1 ≤ nums[mid]=4,左半 [6..8] 是有序的(1…4)。target 2 正好落在这个有序半的范围里,所以缩到左半。
- 17把右指针挪到 mid-1=7,灰掉 mid 及其右边整片,搜索区间收缩到 [6, 7]。
- 18mid=6(值 1)。因为 nums[l]=1 ≤ nums[mid]=1,左半 [6..6] 是有序的(1…1)。target 2 不在这个有序半的范围里,所以去另一半(右半)。
- 19把左指针挪到 mid+1=7,灰掉 mid 及其左边整片,搜索区间收缩到 [7, 7]。
- 20mid=7,nums[7]=2 正好等于 target 2,命中!返回下标 7。
- 21命中的格子绿色高亮。每一步都把区间砍一半,4 步就锁定了答案下标 7,这就是 O(log n)。
- 23mid=5(值 0)。nums[l]=14 > nums[mid]=0,右半 [5..11] 有序。target 5 落在有序半范围内,缩到右半。
- 24左指针挪到 6,区间收缩到 [6, 11]。
- 25mid=8(值 4)。nums[l]=1 ≤ nums[mid]=4,左半 [6..8] 有序。target 5 不在有序半范围内,去另一半(右半)。
- 26左指针挪到 9,区间收缩到 [9, 11]。
- 27mid=10(值 9)。nums[l]=6 ≤ nums[mid]=9,左半 [9..10] 有序。target 5 不在有序半范围内,去另一半(右半)。
- 28左指针挪到 11,区间收缩到 [11, 11]。
- 29mid=11(值 11)。nums[l]=11 ≤ nums[mid]=11,左半 [11..11] 有序。target 5 不在有序半范围内,去另一半(右半)。
- 30左指针挪到 12,区间收缩到 [12, 11]。
- 31当左指针越过了右指针、区间变空,说明 target 5 确实不在数组里,返回 -1。二分天然不会漏判也不会误报。
⚠️ 容易写错的地方
✗ 错:直接套普通二分比大小
✓ 对:必须先判断哪半有序,再判 target 在不在有序半
旋转后整体不单调,nums[mid] 和 target 直接比没有方向性
✗ 错:判范围时边界用 < 还是 <=
✓ 对:有序左半用 nums[l]<=target<nums[mid],右半用 nums[mid]<target<=nums[r]
mid 已在上面比过不等于 target,范围要把 mid 排除、把端点含进来
✗ 错:循环条件用 l<r
✓ 对:要用 l<=r
l==r 时还有最后一个元素没查,漏掉会误返回 -1
完整代码(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 mid
if nums[l] <= nums[mid]: # 左半有序
if nums[l] <= target < nums[mid]:
r = mid - 1 # target 在有序左半
else:
l = mid + 1
else: # 右半有序
if nums[mid] < target <= nums[r]:
l = mid + 1 # target 在有序右半
else:
r = mid - 1
return -1C++
int search(vector<int>& nums, int target){
int l = 0, r = nums.size() - 1;
while(l <= r){
int mid = l + (r - l) / 2;
if(nums[mid] == target) return mid;
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 -1;
}Java
public int search(int[] nums, int target) {
int l = 0, r = nums.length - 1;
while (l <= r) {
int mid = l + (r - l) / 2;
if (nums[mid] == target) return mid;
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 -1;
}复杂度
时间
O(log n)
每步砍掉一半区间,最多 log₂n 步
空间
O(1)
只用 l、mid、r 三个下标,不开额外数组
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 搜索旋转排序数组 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
如果数组里有重复元素(LC81)会怎样?+
当 nums[l]==nums[mid]==nums[r] 时无法判断哪半有序,只能 l++、r-- 收缩一格跳过重复,最坏退化到 O(n)。本题(LC33)保证无重复,所以稳定 O(log n)。
能不能先找到旋转点再二分?+
可以:先 O(log n) 找最小值位置(旋转点),把数组逻辑上还原成两段有序,再在对应段做普通二分。但要两趟二分,不如本题「一趟里判有序半」简洁。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 搜索旋转排序数组 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。