LeetCode 153. & 154. & 33. &81. 寻找旋转排序数组中的最小值 和 搜索旋转排序数组(“旋转”数组的二分查找问题)

2020年8月3日 周一 天气阴 【不悲叹过去,不荒废现在,不惧怕未来】



1. LeetCode153. 寻找旋转排序数组中的最小值

1.1 题目简介

LeetCode 153. 寻找旋转排序数组中的最小值
在这里插入图片描述
首先说一下题目中的“旋转”,其实就是循环右移,“旋转”将数组分为两部分,并且这两部分都是升序

1.2 二分法变形(重点考虑边界问题)

虽然数组被“旋转”了,但是仍然可以用二分法进行求解,只不过需要稍微进行修改。先复习一下传统的二分搜索:

int BinarySearch(vector<int>& nums, int target) {
    int l = 0, r = nums.size()-1;
    while (l <= r) {
        int m = (r + l) / 2;
        if (nums[m] == target) 
			return m;
        else if(nums[m] < target) 
			l = m + 1;
        else 
			r = m - 1;
    }
    return -1;
}

1.2.1 nums[m] < nums[0] 和 nums[m] < nums[len-1] 通过

在这里,我们需要找到数组中最小的元素,首先判断数组是否被“旋转”,如果没有,直接返回数组的首元素即为最小元素。

如前所述,如果数组被“旋转”了,数组会被分为左右两部分,并且这两部分都是升序,而最小元素为数组右部分的第一个元素。我们将nums[m]和数组的首元素nums[0]进行比较,如果nums[m]比较小,那可以肯定m所在的位置为数组右部分,并且不能确定nums[m]是否为最小元素,因此令r = m。反之如果nums[m]比较大,那可以肯定m所在的位置为数组左部分,并且能确定nums[m]不是最小元素,因此令l = m + 1。

这样循环下去,最后会令l和r均指向最小元素所在的位置,因此返回nums[l]和nums[r]都可以。

代码如下:

class Solution {
public:
    int findMin(vector<int>& nums) {
        int len = nums.size();
        int l = 0, r = len-1;
        if(nums[l]<nums[r]) return nums[l];
        while(l<r){
            int m = (l+r)/2;
            if(nums[m]<nums[0])
                r = m;
            else
                l = m + 1;
        }
        return nums[l];
    }
};

另外,如果把nums[m] < nums[0]改成nums[m] < nums[len-1],效果是一样的,因为无论是将nums[m]和首元素还是尾元素进行比较,都能看出nums[m]是属于数组左半部分还是右半部分,从而决定是移动l还是r。

class Solution {
public:
    int findMin(vector<int>& nums) {
        int len = nums.size();
        int l = 0, r = len-1;
        if(nums[l]<nums[r]) return nums[l];
        while(l<r){
            int m = (l+r)/2;
            // nums[m]<nums[0]改成nums[m]<nums[len-1],效果一样
            if(nums[m]<nums[len-1])
                r = m;
            else
                l = m + 1;
        }
        return nums[l];
    }
};

1.2.2 nums[m] < nums[r] 通过,nums[m] < nums[l] 不通过

那么问题来了,我能不能就让nums[m]和nums[r]或nums[l]进行比较呢?如下所示,将代码中改成nums[m] < nums[r],测试了一下是能通过的。

class Solution {
public:
    int findMin(vector<int>& nums) {
        int len = nums.size();
        int l = 0, r = len-1;
        if(nums[l]<nums[r]) return nums[l];
        while(l<r){
            int m = (l+r)/2;
            if(nums[m]<nums[r])
                r = m;
            else
                l = m + 1;
        }
        return nums[l];
    }
};

而将nums[m] < nums[r]改成nums[m] < nums[l]后,程序在提交的时候就报错了:
在这里插入图片描述

分析问题

这是因为,代码里无论是nums[m] < nums[r]还是nums[m] < nums[l],都是为了判断nums[m]是否在数组的右部分,而一旦满足表达式,nums[m]必为数组的右部分,因此r = m决定了右边界r的位置永远在数组的右部分,不会越界到左部分。但是左边界l就不一定那么本分了,如果nums[m]是左部分最后一个元素,那么l = m + 1这行代码执行完之后,l就会指向右部分的第一个元素。

以上面输入的[3,4,5,1,2]为例:

[3,4,5,1,2]

第一次循环nums[m] = 5,nums[l] = 3,nums[m] > nums[r],所以l = m + 1 = 3,r仍然是4,即:l指向1,r指向2。第二次循环,m = (l + r) / 2 = 3,即:nums[m] == nums[l],所以执行l = m + 1,得到l = r = 4。退出循环,输出nums[4]即为2。

所以我们看到,一旦l出现在数组右部分之后,nums[m] < nums[l]这个判断条件就有可能误判nums[m]的位置,使结果出错。那么该如何解决这个问题呢?

*最终优化代码

一个很好的解决办法是:我们在循环条件中一直判断l到r是否为升序,如果是升序,说明l第一次出现在了数组右部分,这时候返回nums[l]即为数组最小元素。这样做也把刚开始判断数组是否为升序融合了进去,可谓一箭双雕。不过需要注意的是,最后返回的只能是nums[l]

代码如下:

class Solution {
public:
    int findMin(vector<int>& nums) {
        int len = nums.size();
        int l = 0, r = len-1;
        // 每次循环判断nums[l]和nums[r]的大小,如果nums[l]<nums[r],则l到r为升序,直接返回nums[l]即为最小值
        while(l<r && nums[l] > nums[r]){
            int m = (l+r)/2;
            if(nums[m]<nums[l])
                r = m;
            else
                l = m + 1;
        }l
        return nums[l];
    }
};

值得一提的是,这样做之后,无论把nums[m] < nums[l]中的nums[l]改成nums[r]或nums[0]或nums[len-1],都是可以的。

2. LeetCode154. 寻找旋转排序数组中的最小值 II

2.1 题目简介

LeetCode 154. 寻找旋转排序数组中的最小值 II
在这里插入图片描述

2.2 二分法变形

2.2.1 nums[m]==nums[r] ?

这回,数组里元素允许重复了。不过解决办法也很简单,我们只需要在上题题解的基础上,加上一个判断条件:如果nums[m]==nums[r],则- -r,也就是把右边界往里面收缩一个位置,这样满足l = r的位置就是最终结果。代码如下:

class Solution {
public:
    int findMin(vector<int>& nums) {
        int len = nums.size();
        int l = 0, r = len-1;
        if(nums[l]<nums[r]) return nums[l];
        while(l<r){
            int m = (l+r)/2;
            if(nums[m]<nums[r])
                r = m;
            else if(nums[m]>nums[r])
                l = m + 1;
            else
                --r;
        }
        return nums[l];
    }
};

2.2.2 nums[m]==nums[l] ?

当然啦,正如我们在1.2.2中提到的,如果想让nums[m]和nums[l]进行比较,只需要把判断数组是否为升序这一步放在循环条件中就行了。相应的,- -r也变成了++l。

代码如下:

class Solution {
public:
    int findMin(vector<int>& nums) {
        int len = nums.size();
        int l = 0, r = len-1;
        while(l<r && nums[l]>=nums[r]){
            int m = (l+r)/2;
            if(nums[m]<nums[l])
                r = m;
            else if(nums[m]>nums[l])
                l = m + 1;
            else
                ++l;
        }
        return nums[l];
    }
};

3. LeetCode33. 搜索旋转排序数组

3.1 题目简介

LeetCode33. 搜索旋转排序数组
在这里插入图片描述
这道题和”LeetCode153. 寻找旋转排序数组中的最小值“很像,只不过这里是搜索一个确定的值是否存在。

3.2 二分法变形

思路

  1. nums[m]把数组分成两部分,其中一部分一定是升序;
  2. 先将nums[m]和nums[l]或nums[r]进行比较,判断哪一部分是升序;
  3. 根据target是否属于升序部分移动边界r或l。

代码如下:

class Solution {
public:
    int search(vector<int>& nums, int target) {
        int l=0, r=nums.size()-1;
        while(l<=r){
            int m = l+(r-l)/2;
            if(nums[m]==target) return m;
            //nums[r]改成nums[l]效果一样,都能判断哪一部分是升序的
            if(nums[m]>=nums[l]){
                // 当搞不清楚边界情况该不该合并时,就分开写
                if(target==nums[l]) return l;
                else if(target>nums[l] && target<nums[m]) r=m-1;
                else l=m+1;
            }
            else{
                // 当搞不清楚边界情况该不该合并时,就分开写
                if(target==nums[r]) return r;
                if(target>nums[m] && target<nums[r]) l=m+1;
                else r=m-1;
            }
        }
        return -1;
    }
};

4. LeetCode81. 搜索旋转排序数组 II

4.1 题目简介

LeetCode81. 搜索旋转排序数组 II
在这里插入图片描述

4.2 二分法变形

思路

大致思路和上一题一样,只不过这次数组中可能有重复元素,处理方法和”2. LeetCode154. 寻找旋转排序数组中的最小值 II“一样:如果中间值nums[m]和边界值nums[r]或nums[l]重复了,就把相应边界往里面收缩一个位置

代码如下:

class Solution {
public:
    bool search(vector<int>& nums, int target) {
        int len = nums.size();
        int l = 0, r = len-1;
        while(l<=r){
            int m = (l+r)/2;
            if (nums[m] == target)
                return true;
				
			// nums[r]也可以改成nums[l],那么--r就变成了++l,下面也是nums[m]和nums[l]进行比较了
            if(nums[m]==nums[r])
                --r;

            else if(nums[m]<nums[r]){
                if(target>nums[m]&&target<=nums[r]) l = m+1;
                else r = m-1;
            }
            else {
                if(target>=nums[l]&&target<nums[m]) r = m-1;
                else l = m+1;
            }
        }
        return false;    
    }
};

注意:nums[m]==nums[r]也可以改成nums[m]==nums[l],那么- -r就相应变成了++l,后面也必须是nums[m]和nums[l]进行比较了。

总结

感觉这类二分搜索法的变形问题非常灵活,细节拉满,一不小心就会出错。我的建议是要多刷一些这类题目,积累经验,二分法变形的题见多了,有时候脑子都不用怎么思考,手自然而然地就把代码写出来了。


参考文献

https://leetcode-cn.com/problems/search-in-rotated-sorted-array/solution/duo-si-lu-wan-quan-gong-lue-bi-xu-miao-dong-by-swe/

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值