这是我参与2022首次更文挑战的第16天,活动详情查看:2022首次更文挑战
题目
33. 搜索旋转排序数组
整数数组 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 。
示例 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-10^4 <= nums[i] <= 10^4nums中的每个值都 独一无二- 题目数据保证
nums在预先未知的某个下标上进行了旋转 -10^4 <= target <= 10^4
进阶: 你可以设计一个时间复杂度为 O(log n) 的解决方案吗?
每日一皮
思路
-
常规解法的话可以直接遍历一次数组,直接使用数组的
findIndex方法一行代码就能搞定,最坏的时间复杂度为O(n); -
不过我们这道题目显然不是考察我们对于数组方法的使用,直接快进到进阶环节,设计一个时间复杂度为
O(log n)的解决方案,不过提前打个预防针,进阶的解法只是在整体思路上的提升,对于这道题的测试用例来说耗时不一定更短,最后再分析为什么会出现这种情况; -
关于有序数组中查找指定元素,我们之前说过最快的是通过二分查找来每次对半切。根据题意,整个数组被分割成了两个有序的子序列,前半部分的值比较大,后面部分的值比较小,我们顺着这个思路分析,先判断目标值比数组的第一个元素大还是小,如果大于的话说明元素出现在前半截,如果小于的话说明元素出现在后半截;
-
然后我们开始实现二分查找,老规矩,如果找到了当前值,直接返回索引,如果当前值小于目标值,那么有两种情况:
4.1 更大的值在后面,普通的二分查找即可;
4.2 更大的值在前面,这时候需要特殊判断;
我们可以通过判断当前元素和数组第一个元素的大小关系,明确我们是在前半段还是后半段,如果是在前半段的话,那么更大的值一定是在它后面的,如果是在后半段的话,那么我们只需要比较数组的最后一个元素和目标值的大小关系即可,如果最后一个元素比目标值小说明目标值在前面。
-
同理,对于当前值大于目标值的,也做同样的处理,然后就是朴实无华的二分查找,找到了目标值所在的为止直接返回,如果找不到就返回-1即可。
实现
/**
* @param {number[]} nums
* @param {number} target
* @return {number}
*/
var search = function(nums, target) {
let left = 0, right = nums.length - 1;
while (left <= right) {
const mid = Math.floor((left + right) / 2);
if (target === nums[mid]) {
return mid;
} else if (target > nums[mid]) {
// 当前值如果小于目标值
// 那么有两种可能,一种是更大值在后面,一种是更大的值在前面,怎么区分呢?
// 可以根据开头的值来判断,当前在前半截还是后半截
// 判断当前在前半截还是后半截
if (nums[0] > nums[mid]) {
// 后半截还需要判断,因为可能最大值都达不到目标
if (nums[nums.length - 1] >= target) {
left = mid + 1;
} else {
right = mid - 1;
}
} else {
left = mid + 1;
}
} else {
// 判断当前在前半截还是后半截
if (nums[0] <= nums[mid]) {
// 前半截还需要再做判断,如果数组的第一个元素比目标值大,说明目标值在后面
if (nums[0] > target) {
left = mid + 1;
} else {
right = mid - 1;
}
} else {
right = mid - 1;
}
}
}
return -1;
};
代码简化
上述过程中的多个if有些可以合并的我们合并在一起,同时把花花绿绿的注释干掉,留下一个最精简版本的答案。
/**
* @param {number[]} nums
* @param {number} target
* @return {number}
*/
var search = function(nums, target) {
let left = 0, right = nums.length - 1;
while (left <= right) {
const mid = Math.floor((left + right) / 2);
if (target === nums[mid]) {
return mid;
} else if (target > nums[mid]) {
// 如果目标值大于当前值
// 只有当前元素在后半截同时后半截的最大值小于目标的情况下往前查找
if (nums[0] > nums[mid] && target > nums[nums.length - 1]) {
right = mid - 1;
} else {
left = mid + 1;
}
} else {
// 如果目标值小于当前值
// 只有当前元素在前半截同时前半截的最小值大于目标的情况下往后查找
if (nums[0] <= nums[mid] && target < nums[0]) {
left = mid + 1;
} else {
right = mid - 1;
}
}
}
return -1;
};
最终结果
然后再回过头来说为什么二分查找的O(logN)的时间会比普通查找的O(N)慢,这是因为我们如果测试的数据量级的问题,只有数据量上去了,时间差异才会有明显的体现,像是你在1-10中查找2,你会发现正常的1 - 2 就找到了,这时候一堆花里胡哨的组合拳并没有啥实际意义。
看懂了的小伙伴可以点个关注、咱们下道题目见。如无意外以后文章都会以这种形式,有好的建议欢迎评论区留言。