壹 ❀ 引
本来今天(2021.4.7)的每日一题是81. 搜索旋转排序数组 II,但今天工作很忙,下班人基本累个半死,题目别说按照二分法的思路做不出来,连题解看了会都没法沉下心去看,不过得到的信息是,本题属于另一道的变体,而且若先了解另一题,对于本题会有较大的帮助,想了想就还是先记录之前的题,题目来自LeetCode33. 搜索旋转排序数组,题目难度同样是中等,题目描述如下:
整数数组 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^4
nums 中的每个值都 独一无二
题目数据保证 nums 在预先未知的某个下标上进行了旋转
-10^4 <= target <= 10^4
今天就熬个夜先记录和理解本题吧,明天再把升级版的题目再补回来。
贰 ❀ 简单分析与二分法
JS的同学可能看到此题,本能想到的就是findIndex了,不过要是用这种来解题,本身就没什么意义了,所以这种投机取巧的做法就不说了,对于算法也没太大提升,我们直接介绍二分法做法。
我在JS leetcode 寻找旋转排序数组中的最小值 题解分析,你不得不了解的二分法一文中,有简单提及二分法,而且比较巧的是,这道题也是旋转数组。关于二分法查找的优点是,每次条件判断后总能舍弃掉一半的元素,从而大大加快查找的效率,二分法的时间复杂度是O(logn)
,我们先上一个查找目标元素的二分法模板:
// 查找目标值二分法模板
function binarySearch(arr, target) {
var low = 0;
var high = arr.length - 1;
while (low <= high) {
var mid = Math.floor((low + high) / 2);
if (target === arr[mid]) {
return mid;
} else if (target > arr[mid]) {
low = mid + 1;
} else {
high = mid - 1;
};
};
return -1;
};
而对于本题来说,较为难受的是数组虽然是有序数组,但是一个经过旋转的有序数组,我们不知道在哪个点进行了旋转,所以一般的二分法在这行不通。
对于常规二分法,我们是根据目标值与mid对比,从而确定目标值在mid的左侧或者右侧(或者运气好直接相等找到了),从而不断缩小范围。但事实上,对于旋转的有序数组依旧有规律可循。
我们以数组[1,2,3,4,5]
为例,我们要找到3,它可能存在旋转情况如下:
如上图,第一行为为旋转,下面四行为此数组可能旋转的所有情况,我们找出mid,根据与nums[0]
的大小对比,可以得知:
- 若mid<nums[0],那么mid在右侧有序序列,比如
[2,3,4]
- 若mid>=nums[0],那么mid在左侧有序序列,比如
[3,4,5]
,注意,为什么是>=后面会解释。
这样我们就已经对于区域做了一次划分,但既然是二分法,自然得舍弃掉一半的元素,此时就得依赖target了,判断依据其实很简单。
假设我们的数组是[5,1,2,3,4]
找3,我们先得知有序序列在右侧,也就是[2,3,4]
,我们将这个范围理解为[mid,end],那么只要target>mid&&target<=end
,那就说明3一定在右侧有序序列中,左边的[5,1]
可以直接舍弃。
接下来我们可以调整左侧边界为mid+1
继续搜索,为什么加1呢?因为如果mid===target
已经返回了,能走到这一步自然mid不会相等,下次调整边界自然可以舍弃掉,用图表示这个过程如下:
有同学可能就想到,你这样解释太过于理想了,如果target在无序那边呢?其实也不冲突,我们还是假设[5,1,2,3,4]
中找1。
很明显由于mid<5,所以有序部分在右侧,但因为target并不满足target>mid&&target<=end
,因此右边界调整为mid-1
,于是我们舍弃了右边部分,得到了[5,1]
。
接下来怎么办?当然还是重复判断哪边为有序部分,哪边不是,我们同样还是找到mid,也就是5,由于此时mid>=nums[0]
,也就是5>=5
,因此mid在左侧有序部分,所以得到了有序部分[5]
以及无序部分[1]
,哎,到这里你是不是知道了为什么是>=
了?找出有序部分的目的,其实就是为了方便我们利用target>mid && target<=end
(假设有序有右侧)来决定放弃哪一部分,如果你的数组不是有序的,target>mid&&target<=end
这个公式你根本没法满足。
那么上面这个过程用图就是下面这样:
解释的够清楚了,直接上代码:
/**
* @param {number[]} nums
* @param {number} target
* @return {number}
*/
var search = function (nums, target) {
let l = 0;
let r = nums.length - 1;
while (l <= r) {
const mid = Math.floor((l + r) / 2);
if (nums[mid] === target) {
return mid;
};
if (nums[mid] >= nums[l]) {
//target 在 [l, mid] 之间
if (target >= nums[l] && target < nums[mid]) {
r = mid - 1;
} else {
//target 不在 [l, mid] 之间
l = mid + 1;
};
} else {
// [mid, r]有序
// target 在 [mid, r] 之间
if (target > nums[mid] && target <= nums[r]) {
l = mid + 1;
} else {
// target 不在 [mid, r] 之间
r = mid - 1;
}
}
}
return -1;
};
总结下解题的核心,第一点,根据mid与nums[0](第一位是可变的,所以其实是nums[左边界])决定哪一边是有序序列,对有序序列套用target>mid && target<=r
从而得知target在不在有序序列这一边,不在自然在无序那一边,继续循环上述步骤,找到最终答案。
真的累了,2点了....睡觉。这题就说到这里了。
嗯....图片貌似有点大,确实困了...就这样吧。