听风是风

学或不学,知识都在那里,只增不减。

导航

JS Leetcode 33. 搜索旋转排序数组题解,图解旋转数组中的二分法

壹 ❀ 引

本来今天(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]的大小对比,可以得知:

  1. 若mid<nums[0],那么mid在右侧有序序列,比如[2,3,4]
  2. 若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点了....睡觉。这题就说到这里了。

嗯....图片貌似有点大,确实困了...就这样吧。

posted on 2021-04-08 01:45  听风是风  阅读(329)  评论(0编辑  收藏  举报