整数数组 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
提示:
1 <= nums.length <= 5000-104 <= nums[i] <= 104nums中的每个值都 独一无二- 题目数据保证
nums在预先未知的某个下标上进行了旋转 -104 <= target <= 104
1. 生活案例:在两段断掉的电梯上找人
想象商场里有一部超长的自动扶梯,原本是从 1 楼直达 10 楼的。结果电梯断成了两截并错位了:
- 上半段:6, 7, 8, 9, 10
- 下半段:1, 2, 3, 4, 5
- 现状:现在的顺序是
[6, 7, 8, 9, 10, 1, 2, 3, 4, 5]。
任务:老板让你找“3号乘客”在哪。
聪明的方法:你站在电梯中间(mid)。
- 你先看你脚下这一截电梯,哪一半是完整的(有序的) ?
- 如果你发现左边这截是完整的(比如从 6 到 10),你就看 3 号在不在 6 到 10 之间。
- 如果不在,那 3 号肯定在另一截断掉的电梯里。
2. 代码实现与详细注释
这是你图片中的代码,我为你加上了“找电梯”逻辑的详细中文注释:
JavaScript
/**
* @param {number[]} nums
* @param {number} target
* @return {number}
*/
var search = function(nums, target) {
let left = 0;
let right = nums.length - 1;
while (left <= right) {
let mid = Math.floor(left + (right - left) / 2);
// 运气好,直接在中间找到了
if (nums[mid] === target) return mid;
// 【关键判断】:先确定哪一半是有序的
if (nums[left] <= nums[mid]) {
// 说明左半部分 [left...mid] 是连贯的、有序的
// 就像电梯的 [6, 7, 8, 9, 10] 这一段
if (nums[left] <= target && target < nums[mid]) {
// 如果目标就在这段有序的范围内,那就把右边界拉回来
right = mid - 1;
} else {
// 否则,目标肯定在右边那截断掉的电梯里
left = mid + 1;
}
} else {
// 说明右半部分 [mid...right] 是连贯有序的
// 就像电梯的 [1, 2, 3, 4, 5] 这一段
if (nums[mid] < target && target <= nums[right]) {
// 如果目标在这段有序的范围内
left = mid + 1;
} else {
// 否则,去左边那截找
right = mid - 1;
}
}
}
// 找遍了也没找到
return -1;
};
3. 核心原理解析
为什么这个逻辑行得通?
在旋转排序数组中,如果你从中间切一刀,一定至少有一半是有序的。
- 如果
nums[left] <= nums[mid],左边有序。 - 如果
nums[left] > nums[mid],那么右边一定有序。
通过这个特性,我们可以利用有序的那一半来做范围判断。如果 target 不在有序的那一半,它就一定在无序的那一半。
复杂度分析
- 时间复杂度:。虽然逻辑变复杂了,但本质还是每次砍掉一半的可能性,这就是二分查找的威力。
- 空间复杂度:。只用了几个指针变量。