JS leetcode 寻找旋转排序数组中的最小值 题解分析,你不得不了解的二分法

壹 ❀ 引

堕落了一天,那么接着来刷leetcode,今天作的一题不算复杂,题目来自leetcode153. 寻找旋转排序数组中的最小值,题目描述以下:javascript

假设按照升序排序的数组在预先未知的某个点上进行了旋转。html

( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。java

请找出其中最小的元素。算法

你能够假设数组中不存在重复元素。数组

示例 1:app

输入: [3,4,5,1,2]
输出: 1

示例 2:.net

输入: [4,5,6,7,0,1,2]
输出: 0

在以前JS leetcode 旋转数组 题解分析一文中,咱们已经作过旋转数组的题目,因此这里所说的旋转其实只是将数组尾部的元素依次加入数组头部的操做。咱们简单分析题目,再说怎么实现。code

贰 ❀ 解题思路

贰 ❀ 暴力解题

由题目提供的信息可知,数组为升序排列数组,并且在某个未知的点进行了旋转,因此它可能没转。htm

不过但站在找出数组中最小元素来讲,咱们能够不考虑这些条件,直接祭出Math.min()大法:blog

/**
 * @param {number[]} nums
 * @return {number}
 */
var findMin = function(nums) {
    return Math.min(...nums);
    
    //ES5
    //return Math.min.apply(null, nums);
};

因为数组本来就是排序好的,只是可能进行了旋转,咱们能够可耻的将数组再排序而后取出索引0位元素:

/**
 * @param {number[]} nums
 * @return {number}
 */
var findMin = function (nums) {
    return nums.sort((a, b) => a - b)[0];
};

虽然能达到目的,总以为差了点意思,提交后发现执行时间排名也很低,说明有更好的作法。

贰 ❀ 什么是二分法?

在看了官方的解题思路后,发现本题用二分法查找会更好,不过对于我来讲,二分法是啥都比较疑惑,因此这里先简单说说什么是二分法。

好比,咱们要在数组[1,2,3,4,5,6,7,8,9]里面找到目标9的下标,按照常规遍历,咱们只有从头遍历到最后一位才能找到,时间复杂度为O(n),使用二分法就不同,以下:

/**
 * @desc 二分法查找目标元素索引
 * @param {*} arr 数组
 * @param {*} target 目标元素
 */
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;
        };
    };
    // 没找到返回-1
    return -1;
};
binarySearch([1, 2, 3, 4, 5, 6, 7, 8, 9], 9); //8

咱们一开始就能够按照某个特定规则找到数组中的中间项元素,从而将数组一分为二,而后将中间项与目标元素比较,好比一开始咱们找到了5,因为比目标元素9小,因此9确定在5右边的数组中。

那么下次遍历就从[6,7,8,9]开始,此次中间元素找到7,仍然比9小,继续上述操做,又在[8,9]中寻找。直到最后找到目标元素9,从而获取到索引。

你看,这样对半分的查找,是否是比咱们常规从头至尾的遍历要快不少,二分法的时间复杂度为O(logn),注意,二分法适合有序序列,否则咱们也不知道下次应该去左边仍是右边比较了。

贰 ❀ 经过二分法解决此题

前面说了,二分法适合有序数组,这样咱们才好根据特定条件来决定下次应该从哪边开始,好比上文中的target > arr[mid]。而本题虽然也是有序,但很遗憾的是数组通过了旋转操做,因此通常的二分法并不适用。

比较麻烦的是,虽然题目说了作了旋转,不保证数组旋转了一整圈结果并没有变化的状况。因此第一步咱们能够先判断数组到底有没有被旋转,判断条件很简单

已知数组是有序数组,若是数组发生了旋转,那么数组第一位必定大于最后一位,反之若是数组未发生旋转,那么第一位必定小于最后一位,这种状况咱们直接返回第一位便可。

那若是数组已发生了旋转,咱们又该怎么判断呢,其实有一个这样的特色:

纵观整个数组,咱们以相邻两个元素来看,假设当前中间元素为mid,若是arr[mid-1]大于arr[mid],那么mid为咱们想要的元素。或者arr[mid]大于arr[mid+1],那么此时mid+1是咱们想要找的元素。为啥这么说,由于数组虽然旋转了,但相邻且知足如上任意条件之一的状况只存在一次,不信你们随便写个有序数组看。

因此每次肯定中间元素,咱们都得走以下条件,只要知足其一便是咱们想要找的元素。

if (nums[mid] > nums[mid + 1]) {
    return nums[mid + 1];
};

if (nums[mid - 1] > nums[mid]) {
    return nums[mid];
};

这是找到目标元素的条件,那不知足咱们如何知道应该查找左边数组仍是右边数组呢?其实有这样一个规律:

如上图,咱们将9=>1之间称为变化点,变化点左边的元素都必定比数组第一位元素大,变化点右边的全部元素,必定比数组第一位元素小。

因此找到中间元素mid,若是mid比第一位还大,那说明咱们应该去数组右边找,若是mid比第一位还小,那么应该去左边找。

你可能在想,万一我这个mid第一次就是最小元素咋办,若是运气真这么好,它早就被咱们上面定的两个条件之一给返回了,能走到这一步说明这个mid必定不是咱们想找的目标元素,这才要分去哪边找啊。

那么咱们上代码:

/**
 * @param {number[]} nums
 * @return {number}
 */
var findMin = function(nums) {
    // 假设数组只有一项,直接返回
    if (nums.length == 1) {
        return nums[0];
    };

    var left = 0,
        right = nums.length - 1;
    // 假设数组最后一项大于第一项,说明数组未旋转,直接返回
    if (nums[right] > nums[0]) {
        return nums[0];
    };

    // 既然能走到这一步,那说明数组必定旋转了,套用以前的规则,使用二分法进行查找
    while (right >= left) {
        // Find the mid element
        var mid = Math.floor((left + right) / 2);
        // 知足以下条件之一说明就是最小元素,直接返回便可
        if (nums[mid] > nums[mid + 1]) {
            return nums[mid + 1];
        };
        if (nums[mid - 1] > nums[mid]) {
            return nums[mid];
        };
        // 比较当前中间元素与第一位
        if (nums[mid] > nums[0]) {
            // 若是要大,那就去右边找
            left = mid + 1;
        } else {
            // 反之就去左边找
            right = mid - 1;
        };
    };
    return -1;
};

执行图解以下:

其实这段代码我分析了好久,对于我以为比较难的是什么状况返回mid,我以为mid若是是最小,它必定比mid+1小,但这样是不成立的,例如1比2小,2也比3小,这个条件无法用。

因此官方分析我以为让我佩服的是,整个数组中,当mid是1时,mid-1必定比mid大,整个数组你找不出第二个这样的状况。同理,当mid是9时,mid必定比mid+1大,你一样找不出第二个这样的状况...

因此变化点相关的两个元素9和1才是解题关键。

那么关于本题就分析到这里了。

另外,二分法参考以下:

算法——二分法查找(binarySearch)

相关文章
相关标签/搜索