壹 ❀ 引
早在10个月前,也就是去年,我记录了JS leetcode 寻找旋转排序数组中的最小值 题解分析,你不得不了解的二分法一题,那么这篇文章记录它的升级版,来自LeetCode154. 寻找旋转排序数组中的最小值 II,但是我现在回头看之前这篇文章,解题思路更像是找规律,有点难以记忆。我在前几天记录的另外两篇旋转数组中使用了相同的解题思路,所以为了思路上的统一,我想用相同的思路先把153. 寻找旋转排序数组中的最小值先重新梳理一遍,再来看它的升级版。回归正题,154. 寻找旋转排序数组中的最小值 II题目描述如下:
已知一个长度为 n 的数组,预先按照升序排列,经由 1 到 n 次 旋转 后,得到输入数组。例如,原数组 nums = [0,1,4,4,5,6,7] 在变化后可能得到:
若旋转 4 次,则可以得到 [4,5,6,7,0,1,4]
若旋转 7 次,则可以得到 [0,1,4,4,5,6,7]
注意,数组 [a[0], a[1], a[2], ..., a[n-1]] 旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], ..., a[n-2]] 。给你一个可能存在 重复 元素值的数组 nums ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。
示例 1:
输入:nums = [1,3,5] 输出:1
示例 2:
输入:nums = [2,2,2,0,1] 输出:0
提示:
n == nums.length
1 <= n <= 5000
-5000 <= nums[i] <= 5000
nums 原来是一个升序排序的数组,并进行了 1 至 n 次旋转进阶:
这道题是 寻找旋转排序数组中的最小值 的延伸题目。
允许重复会影响算法的时间复杂度吗?会如何影响,为什么?
贰 ❀ 思路分析与二分法
OK,我们还是先重新复习下153. 寻找旋转排序数组中的最小值这题,此题与升级版的题目唯一区别是数组中不存在重复项,即是一个数字不重复的被旋转的升序数组,让你找到数组中的最小值。
因为前面我们已经做了两题关于旋转数组的题目,已经总结出了一种二分法的思路,这里直接再次套用这种解题思路,比如要找到[4,5,0,1,2,3]
中的最小值,我们依然可以用mid与nums[0]
进行比较,如果mid>=nums[0]
说明升序部分在左侧,如果mid<nums[0]
说明升序部分在右侧,我们用图画出来,感受下这个规律:
图1
如上图,通过中间数mid与nums[0]
的大小比较就能知道升序在哪一边。如果升序在这一边,那么最小值一定在不是升序的这一边,看图自己感受下。当然有个特例,数组没旋转的情况下,也满足mid>=nums[0]
,但最小值其实在左侧,没关系,因为只有未旋转的数组nums[0]<=nums[length-1]
,比如:
let arr = [1];
arr[0]<=arr[arr.length-1]; //true
let arr = [1,2]
arr[0]<=arr[arr.length-1]; //true
让我们来实现这段代码:
var findMin = function (nums) {
let n = nums.length;
let l = 0;
let r = n - 1;
// 数组未旋转或者数组只有一位的情况
if (nums[l] <= nums[r]) {
return [nums[l]];
};
// 能走到这,说明数组一定旋转了
while (l < r) {
let mid = Math.floor((l + r) / 2);
// 如果mid>=nums[0],升序在左侧,最小在右侧,修改l的指针
if (nums[mid] >= nums[0]) {
// 为什么要加1,请看图感受
l = mid+1;
} else {
//反之升序在右侧,最小值在左侧,修改r指针
r = mid;
}
}
return nums[l] <= nums[r] ? nums[l] : nums[r];
};
我们来解释下这段代码中的几个注意点,第一个问题,为什么是nums[mid] >= nums[0]
,因为我们取mid是向下取整,比如数组[2,1]
,mid取的其实是nums[0]
也就是2,因为nums[mid]
其实等于nums[0]
,其实就是为了兼容这种情况。
第二个问题,为什么l=mid+1
,而r
不是mid-1
呢?其实看我们上面列举的旋转的数组图示,你会发现当mid>=nums[0]
时,由于升序在左侧,最小值在右侧,而且最小一定不可能是mid,所以直接mid+1
,不然假设数组是[2,1]
的情况,你会发现由于l=mid
会一直等于0,然后陷入死循环。
而l
不是mid-1
看图其实也很清楚,因为有可能mid就是最小值,所以我们不能舍弃掉,这也算是画图找规律的一种,但整体思路其实还是与之前两道题相同。
OK,那么到这里我们用之前mid与nums[0]
比较的思路,重新把这道题解了下,算是把思路给统一了,不然一道题一个思路也确实难以记忆。
那么回到它的升级版,也就是存在重复数的情况,我们甚至可以暴力点,直接数组去重后套用上面的代码,比如:
/**
* @param {number[]} nums
* @return {number}
*/
var findMin = function (nums) {
// 去重
nums = [...new Set(nums)]
// 下面不变
let n = nums.length;
let l = 0;
let r = n - 1;
// 数组未旋转或者数组只有一位的情况
if (nums[l] <= nums[r]) {
return [nums[l]];
};
// 能走到这,说明数组一定旋转了
while (l < r) {
let mid = Math.floor((l + r) / 2);
// 如果mid>=nums[0],升序在左侧,最小在右侧,修改l的指针
if (nums[mid] >= nums[0]) {
// 为什么要加1,请看图感受
l = mid+1;
} else {
//反之升序在右侧,最小值在左侧,修改r指针
r = mid;
}
}
return nums[l] <= nums[r] ? nums[l] : nums[r];
};
当然既然题目都给了重复数字,咱们直接去重多少差点意思,如果不去重我们如何解决这道题呢?
举个例子,假设数组为[3,1,3]
,上面的代码会直接挂掉,因为一开始就满足了nums[l] <= nums[r]
,其实在JS Leetcode 81. 搜索旋转排序数组 II 题解,补救二分法的可行性中,我们就解释了重复元素导致二分法特性丢失的问题,而挽救这种特性的办法就是,当数组的nums[l]===nums[r]
时,我让任意一方的指针进行缩进,比如r++
即可,因为给无数个3和给一个3,并不会影响到最终的结果判断。
所以我们修改下上方的代码,不用去重就是这样:
var findMin = function (nums) {
let n = nums.length;
let l = 0;
let r = n - 1;
while (l < r) {
// 只要左右指针相同,就让r--,比如[3,1,3]就会变成[3,1]
if (nums[l] <= nums[r]) {
return [nums[l]];
};
// 这个移进来了,如果放外面可能有[3,1,3]这种,同时也用于解决1个元素的情况以及未旋转的情况
if (nums[l] === nums[r]) {
r--;
break;
};
let mid = Math.floor((l + r) / 2);
// 如果mid>=nums[0],升序在左侧,最小在右侧,修改l的指针
if (nums[mid] >= nums[0]) {
// 为什么要加1,请看图感受
l = mid + 1;
} else {
//反之升序在右侧,最小值在左侧,修改r指针
r = mid;
}
}
return nums[l] <= nums[r] ? nums[l] : nums[r];
};
那么本文就到这里了,相关系列题型可见下方。
JS leetcode 153. 寻找旋转排序数组中的最小值 题解分析,你不得不了解的二分法