LeetCode 面试题 10.03. 搜索旋转数组
题目描述

题意分析
数组原本非递减,旋转后最多出现一处从大到小的断点。要查找
target,不存在时返回-1;存在重复值时,必须返回它在当前数组中的最小下标。普通二分依赖整个区间有序,本题只能先判断哪一半有序,再通过该半区的取值范围排除元素。重复值可能使断点的位置无法判断,此时不能强行砍掉一半;中点命中也不能直接返回,因为左边可能还有目标。
解法:含重复元素的旋转二分
核心思路
[!blue]
left、right表示当前候选闭区间。始终保证:left之前没有目标;如果目标存在,它的最小下标还在[left, right]中。因此每轮先检查arr[left],命中时就能立即返回。如果
arr[mid] == target,最早出现的位置只能在[left, mid]中,令right = mid保留这个已知答案,并继续查找更小下标。左端已经检查过,所以此时mid > left,区间仍会缩小。中点不命中时,比较
arr[left]与arr[mid]:
arr[left] < arr[mid]:左半区没有跨过断点,因而有序。目标落在两端值之间,就保留左半区;否则左半区没有目标,可以整体丢弃。两个端点已确认不等于目标,所以范围两端都用严格不等号。arr[left] > arr[mid]:断点已经出现在左半区,右半区必定有序。目标满足arr[mid] < target <= arr[right]时保留右半区。此时左半区断点前的值不小于arr[left],断点后的值不大于arr[mid];右段的值又不大于arr[left],且左端已经排除,因此不会漏掉左半区中更早的目标。目标不在这个范围内时,丢弃右半区。arr[left] == arr[mid]:相等的端点无法揭示中间是否有断点。但arr[left]已确认不是目标,所以只执行left++,安全排除这一个位置。每种分支都保留最早的目标,同时让区间缩小。最终左端命中即得到答案;区间变空则说明目标不存在。
解题步骤
- 初始化闭区间
[left, right],循环条件为left <= right;空数组会直接返回-1。- 每轮先检查左端点。若
arr[left] == target,根据不变量它就是全局最小下标。- 若中点命中,令
right = mid继续向左找。此时mid > left,所以区间一定缩小。- 若
arr[mid] > arr[left],说明[left, mid]有序:target落在(arr[left], arr[mid])之间才可能在左半边,于是right = mid - 1;否则left = mid + 1。- 若
arr[mid] < arr[left],说明旋转点在(left, mid],[mid, right]有序:target落在(arr[mid], arr[right]]之间才可能在右半边,于是left = mid + 1;否则right = mid - 1。注意右端要取闭区间<=,否则会漏掉恰好等于arr[right]的目标。- 若
arr[left] == arr[mid],执行left++消除重复值造成的歧义。- 循环结束仍未命中,返回
-1。
代码实现
class Solution {
public int search(int[] arr, int target) {
int left = 0;
int right = arr.length - 1;
while (left <= right) {
// 左端点命中即是最小下标,区间左侧已被证明不含 target。
if (arr[left] == target) {
return left;
}
int mid = left + (right - left) / 2;
if (arr[mid] == target) {
// 左边可能还有更早的出现位置,只收缩右边界。
right = mid;
} else if (arr[mid] > arr[left]) {
if (arr[left] < target && target < arr[mid]) {
right = mid - 1;
} else {
left = mid + 1;
}
} else if (arr[mid] < arr[left]) {
if (arr[mid] < target && target <= arr[right]) {
left = mid + 1;
} else {
right = mid - 1;
}
} else {
// 重复值遮蔽有序区间时,只能安全地丢掉一个左端点。
left++;
}
}
return -1;
}
}
func search(arr []int, target int) int {
left := 0
right := len(arr) - 1
for left <= right {
// 左端点命中即是最小下标。
if arr[left] == target {
return left
}
mid := left + (right-left)/2
if arr[mid] == target {
// 继续向左找更小的下标。
right = mid
} else if arr[mid] > arr[left] {
if arr[left] < target && target < arr[mid] {
right = mid - 1
} else {
left = mid + 1
}
} else if arr[mid] < arr[left] {
if arr[mid] < target && target <= arr[right] {
left = mid + 1
} else {
right = mid - 1
}
} else {
// 无法判断有序侧,只丢掉一个左端点。
left++
}
}
return -1
}
复杂度分析
- 时间复杂度:每轮都能判断有序侧时为 $O(\log n)$;最坏为 $O(n)$,因为相等分支每次只排除一个元素。数组全相等而目标不存在时,就会出现这种退化。
- 空间复杂度:$O(1)$,只用了
left、right、mid三个下标变量,没有递归也没有额外容器。
关键点总结
[!green]
- 本题必须返回最小下标,这是它区别于 33、81 的核心考点;命中
mid时收缩右边界而不是直接返回,是解法的关键改动。- 循环开头的
arr[left] == target检查一举两得:既让返回值天然最小,又为「arr[left] == arr[mid]时left++」提供了安全性依据。- 只有证明被丢弃的左半区没有目标,才能移动
left;缩小right时则必须保留可能的最早目标。- 重复元素会遮蔽有序半区的判断,此时只能排除已确认不等于目标的左端,最坏复杂度因此退化。
易错点总结
[!yellow]
- 中点命中就返回,会漏掉左边下标更小的相同元素;应保留中点并继续向左找。
- 保留
right=mid却删掉左端命中检查,收缩到命中单点时可能不再前进。- 左端与中点相等就跨过半区,可能漏掉中间藏着的目标;只能保守移一格。
- 判断右侧有序段时要包含右端值,目标可能恰好等于
arr[right]。- 重复值使最坏复杂度线性,不能保证每次都能排除一半。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 81. 搜索旋转排序数组 II | 中等 | 同样允许旋转数组含重复值,原题只返回是否存在,本题必须返回最小下标,命中后不能直接忽略更左候选。 |
| 33. 搜索旋转排序数组 | 中等 | 无重复值时判断有序半区更直接,本题重复元素会遮蔽边界并影响最早位置的选择。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!