问题简介
题目描述
整数数组 nums 按升序排列,数组中的值 互不相同。
在传递给函数之前,nums 在某个未知的下标 k(0 <= k < nums.length)上进行了 旋转 ,使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]](下标 从 0 开始 计数)。例如,[0,1,2,4,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2]。
给你 旋转后的数组 nums 和一个整数 target,如果 nums 中存在这个目标值 target,则返回它的下标,否则返回 -1。
你必须设计一个时间复杂度为 O(log n) 的算法解决此问题。
示例说明
✅ 示例 1:
输入:nums = [4,5,6,7,0,1,2], target = 0
输出:4
✅ 示例 2:
输入:nums = [4,5,6,7,0,1,2], target = 3
输出:-1
✅ 示例 3:
输入:nums = [1], target = 0
输出:-1
解题思路
📌 核心观察:
虽然数组被旋转了,但它仍然由两个有序子数组 组成。我们可以利用二分查找的思想,在每次迭代中判断哪一半是有序的,并据此决定搜索方向。
✅ 方法一:改进的二分查找(推荐)
步骤如下:
- 初始化
left = 0,right = nums.length - 1。 - 当
left <= right:- 计算中点
mid = (left + right) / 2。 - 如果
nums[mid] == target,直接返回mid。 - 判断左半部分是否有序(即
nums[left] <= nums[mid]):- 如果是,则检查
target是否在[nums[left], nums[mid])范围内:- 若在,缩小右边界:
right = mid - 1; - 否则,搜索右半部分:
left = mid + 1。
- 若在,缩小右边界:
- 如果左半部分无序,则右半部分一定有序:
- 检查
target是否在(nums[mid], nums[right]]范围内:- 若在,
left = mid + 1; - 否则,
right = mid - 1。
- 若在,
- 检查
- 如果是,则检查
- 计算中点
- 循环结束仍未找到,返回
-1。
💡 关键点:
- 由于数组元素互不相同,
nums[left] == nums[mid]只有在left == mid时成立,此时仍可视为左半有序。 - 每次都能排除一半的搜索空间,保证
O(log n)时间复杂度。
❌ 方法二:线性扫描(不满足题目要求)
- 直接遍历数组,时间复杂度
O(n),不符合O(log n)要求,仅作对比。
💡 方法三:先找旋转点再二分(可行但略复杂)
- 先用二分查找找到最小值(即旋转点)的位置
pivot。 - 判断
target应该在前半段还是后半段。 - 在对应段内进行标准二分查找。
虽然也是
O(log n),但需要两次二分,代码更复杂,不如方法一直接。
代码实现
Java Go
java
class Solution {
public int search(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
return mid;
}
// 左半部分有序
if (nums[left] <= nums[mid]) {
if (target >= nums[left] && target < nums[mid]) {
right = mid - 1;
} else {
left = mid + 1;
}
}
// 右半部分有序
else {
if (target > nums[mid] && target <= nums[right]) {
left = mid + 1;
} else {
right = mid - 1;
}
}
}
return -1;
}
go
func search(nums []int, target int) int {
left, right := 0, len(nums)-1
for left <= right {
mid := left + (right-left)/2
if nums[mid] == target {
return mid
}
// 左半部分有序
if nums[left] <= nums[mid] {
if target >= nums[left] && target < nums[mid] {
right = mid - 1
} else {
left = mid + 1
}
} else {
// 右半部分有序
if target > nums[mid] && target <= nums[right] {
left = mid + 1
} else {
right = mid - 1
}
}
}
return -1
}
示例演示
以 nums = [4,5,6,7,0,1,2], target = 0 为例:
| 步骤 | left | right | mid | nums[mid] | 有序部分 | target 范围判断 | 新边界 |
|---|---|---|---|---|---|---|---|
| 1 | 0 | 6 | 3 | 7 | 左有序 | 0 ∉ [4,7) | left=4 |
| 2 | 4 | 6 | 5 | 1 | 右有序 | 0 ∈ (1,2]? 否 | right=4 |
| 3 | 4 | 4 | 4 | 0 | --- | 找到! | return 4 |
✅ 成功找到目标值。
答案有效性证明
- 正确性 :每次迭代都基于"至少有一半是有序的"这一性质,通过判断
target是否落在有序区间内来决定搜索方向,逻辑严密。 - 终止性 :每次循环要么找到目标,要么缩小搜索区间(
left或right移动),最终left > right退出。 - 覆盖边界 :包括单元素、未旋转(
k=0)、target在旋转点等边界情况均能处理。
复杂度分析
| 项目 | 复杂度 |
|---|---|
| ✅ 时间复杂度 | O(log n) ------ 每次排除一半元素 |
| ✅ 空间复杂度 | O(1) ------ 仅使用常数额外空间 |
问题总结
📌 关键收获:
- 旋转排序数组虽整体无序,但局部有序,可结合二分查找。
- 判断哪一半有序是解题突破口。
- 条件判断需严谨,注意边界(如
<=vs<)。
💡 扩展思考:
- 若数组允许重复元素(如 LeetCode 81 题),则
nums[left] == nums[mid]时无法判断哪边有序,需特殊处理(如left++)。 - 本题是"在部分有序结构中应用二分查找"的经典范例,对理解二分思想的灵活性很有帮助。
✅ 掌握此题,就掌握了处理"旋转数组"类问题的核心技巧!
github地址: https://github.com/swf2020/LeetCode-Hot100-Solutions