一、题目要求(LeetCode 33)
给定一个 可能经过旋转的升序数组 nums(数组中 不存在重复元素),以及一个目标值 target。
要求在 O(log n) 时间复杂度内找到目标值的下标,若不存在则返回 -1。
示例 1:
输入:nums = [4,5,6,7,0,1,2], target = 0
输出:4
示例 2:
输入:nums = [5,6,7,8,9,1,2,3], target = 9
输出:4
旋转排序数组的特点是:
- 原数组是严格递增的
- 在某个位置被“切断”并旋转
- 整体不再完全有序,但 一定存在一段连续有序区间
二、解题思路:在旋转数组中做二分查找
普通二分查找依赖的是:
整个数组有序
而本题中数组只满足:
任意时刻,
mid的左边或右边 至少有一侧是有序的
核心思路可以总结为三步:
-
使用二分找到
mid -
判断哪一侧是 有序区间
-
判断
target是否落在该有序区间的 数值范围内- 在 → 缩小到这一半
- 不在 → 去另一半继续查找
关键点:
不是看“mid 左右还有没有数”,而是看“target 是否在有序区间的值域中”
三、完整代码实现
class Solution {
public int search(int[] nums, int target) {
int n = nums.length;
int left = 0;
int right = n - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
// 找到目标
if (nums[mid] == target) {
return mid;
}
// 左半部分有序
if (nums[left] <= nums[mid]) {
// target 落在左半部分的值域内
if (nums[left] <= target && target < nums[mid]) {
right = mid - 1;
} else {
left = mid + 1;
}
}
// 右半部分有序
else {
// target 落在右半部分的值域内
if (nums[mid] < target && target <= nums[right]) {
left = mid + 1;
} else {
right = mid - 1;
}
}
}
return -1;
}
}
四、逐行逻辑解析
1. 初始化左右指针
int left = 0;
int right = n - 1;
维护一个标准的闭区间 [left, right]。
2. 计算中点并判断是否命中
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
return mid;
}
这是最理想的情况,直接返回结果。
3. 判断哪一半是有序的
if (nums[left] <= nums[mid]) {
说明从 left 到 mid 是 递增有序的。
否则:
else {
说明右半部分 [mid, right] 是有序区间。
4. 左半部分有序时的处理
if (nums[left] <= target && target < nums[mid]) {
right = mid - 1;
} else {
left = mid + 1;
}
含义是:
- 如果
target在左半部分的 数值范围 内 - 就可以直接丢弃右半部分
- 否则目标一定在右侧
5. 右半部分有序时的处理
if (nums[mid] < target && target <= nums[right]) {
left = mid + 1;
} else {
right = mid - 1;
}
逻辑与左半部分完全对称,只是判断区间不同。
6. 未找到目标值
return -1;
循环结束仍未命中,说明数组中不存在该值。
五、关于常见疑惑的解释
很多人在理解这道题时会问:
如果 mid 落在左边,但 mid 的右边还有数,而且 target 正好在右边,怎么办?
答案是:
我们从来不是根据“左右有没有数”来判断的,而是根据“哪一半是有序的 + target 是否在该区间的值域中”。
只要 target 不在当前有序区间的数值范围内,就可以整段排除,绝不会漏解。
六、总结
-
LeetCode 33 的本质是 二分查找的变形
-
旋转数组的关键特征是:局部有序
-
每一轮二分:
- 一定存在一侧是有序的
-
判断方向时:
- 不看下标
- 只看 target 是否落在有序区间的数值范围
-
时间复杂度:
O(log n)
空间复杂度:O(1)
这道题是理解“二分查找进阶”的关键题,
真正吃透它,后面所有旋转数组、区间二分问题都会顺很多。