LeetCode 二分法在旋转数组应用系列

本文介绍了一类特殊的数组——旋转数组,并通过四个典型题目详细解析了如何利用二分查找法解决旋转数组上的搜索和极值查找问题。文章还探讨了数组中有无重复元素对算法的影响。

总结

这个系列整理一下旋转数组,就是原始单调不减数组,在预先未知的某个下标 k(0 <= k < nums.length)上进行了 旋转,使数组变为 [nums[k], nums[k+1], …, nums[n-1], nums[0], nums[1], …, nums[k-1]]

针对这种数组,两个要求,一个找到分界点k,另一个是给一个target,找到这个target的位置。然后针对原数组是否有重复还需要考虑一下。这个系列用遍历肯定是能解的,但是考察点肯定是二分~

题解

33. 搜索旋转排序数组

无重复数组,找target

传统的升序数组上,只要确定了nums[mid]与target相比大小,就可以确定是调整left还是right。
但是旋转数组有个问题,我们无法确定边界在哪儿 ,所以先确定边界在左边还是右边(如果有的话), 在mid的左边的话,有nums[mid] < nums[left]。在mid的右边的话,有nums[mid] > nums[left]。
可以分为三种情况讨论
(1)nums[left] < nums[mid] < nums[right]
整体升序
(2)nums[left] < nums[mid] > nums[right]
左半部分升序
(3)nums[left] > nums[mid] < nums[right]
右半部分升序
换言之,对于一个旋转数组,mid可以把数组分成一个升序数组和旋转数组
(1)对于nums[mid] >= nums[left]
那么有mid的左边肯定是个升序数组,判断这个升序数组中有没有target还是很容易的~在的话就调整右边界。反之就在右边的旋转数组。调整左边界。
(2)对于nums[mid] < nums[left]
那么有mid的右边肯定是个升序数组,判断这个升序数组中有没有target还是很容易的~在的话就调整左边界。反之就在左边的旋转数组。调整右边界。
总结一下,与普通的二分法的区别,我们先判断一下,mid的左边还是右边是旋转数组,然后在判断target在哪一边的时候,是要去判断升序数组的那一边能不能包含target,这样相对容易些。

class Solution {
public:
    int search(vector<int>& nums, int target) {
        int n = nums.size();
        if(n == 0) return -1;
        if(n == 1) return nums[0] == target ? 0 : -1;
        int left = 0, right = n-1;
        while(left <= right){
            int mid = (left + right) >> 1;
            if(nums[mid] == target) return mid;
            if(nums[left] <= nums[mid]){
                if(nums[mid] > target && nums[left] <= target)
                    right = mid-1;
                else
                    left = mid+1;
            }
            else{
                if(nums[mid] < target && nums[right] >= target)
                    left = mid+1;
                else
                    right = mid-1;
            }
        }
        return -1;
    }
};

81. 搜索旋转排序数组 II

有重复,找target
有重复的话,其他情况都不影响,主要影响的是nums[mid] 与nums[target]相等的时候,这个时候是没法确定到底分界线在哪儿的。
上一题中nums[mid] 与nums[target]相等时,由于不重复,只会有一种情况,就是mid=target,此时把左部分看做是升序序列一点问题没有。
但这一题中我举两个例子
1, 1, 4, 1
1, 3, 1, 1, 1, 1
这两种情况都是nums[mid] 与nums[target]相等,但是很明显一个分界线在左部分,一个在右部分。
所以对于nums[mid] 与nums[target]相等的情况需要讨论一下,遇到相等就把left前移一步。

class Solution {
public:
    bool search(vector<int>& nums, int target) {
        int n = nums.size();
        if(n == 0) return false;
        if(n == 1) return nums[0] == target;
        int left = 0, right = n-1;
        while(left <= right){
            int mid = left + (right - left) / 2;
            if(nums[mid] == target) return true;
            if(nums[left] == nums[mid]){ 
                ++ left;
                continue;
            }
            if(nums[left] < nums[mid]){
                if(nums[left] <= target && target < nums[mid])
                    right = mid-1;
                else
                    left = mid+1;
            }
            else{
                if(nums[mid] < target && target <= nums[right])
                    left = mid+1;
                else
                    right = mid-1;
            }
        }
        return false;
    }
};

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

与上一种类型不同,这题找最小值
同理三种情况
(1)nums[left] < nums[mid] < nums[right]
最小值在左半部分
(2)nums[left] < nums[mid] > nums[right]
分界线在mid和right之间,最小值在右半部分
(3)nums[left] > nums[mid] < nums[right]
分界线在left和mid之间,最小值在左半部分(包括mid处)

不难发现最小值在左半部分的情况为(1)(3),我们只需要比较nums[mid]和nums[right]即可。

这个二分法用的是左闭右闭,但是在left与right相等的情况就跳出循环,收缩右边界的时候,对于情况(3),mid本身可能就是最小值,所以收缩的时候只能right=mid,而不能right=mid-1。

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

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

上一种不重复的情况,永远不会有nums[mid]=num[right],因为mid永远是偏向左边的(除法影响),只有当left=right,才会有nums[mid]=num[right],而left=right就已经跳出循环了。
但是面对有重复元素,就会有nums[mid]=num[right],这里我们的处理的方式与上一题一样,将右边界减一就可以~

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

寻找旋转排序数组中的最大值

扩展一下,如果是最大值呢?先讨论无重复的问题
还是3种情况
(1)nums[left] < nums[mid] < nums[right]
最大值在右半部分
(2)nums[left] < nums[mid] > nums[right]
分界线在mid和right之间,最大值在右半部分(包括mid处)
(3)nums[left] > nums[mid] < nums[right]
分界线在left和mid之间,最大值在左半部分
可以发现(1)(2)都在右半部分,缩小左边界就可以,这两种情况下比较nums[left]与nums[mid]。
但是这里需要注意的一点是,为了避免无限循环,在left=right+1,此时肯定不能让mid = left。那我们在选取mid的选取偏向右边的就可以。

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

寻找旋转排序数组中的最大值II

那对于有重复的!
会有nums[mid]=num[left],这里我们的处理的方式与上一题一样,将左边界加一就可以~

class Solution {
public:
    int findMin(vector<int>& nums) {
        if(nums.size() == 0) return nums[0];
        int n = nums.size();
        int left = 0, right = n-1;
        while(left < right){
            int mid = left + (right + 1 - left) / 2;
            if(nums[left] == nums[mid])
                ++ left;
            else if(nums[left] < nums[mid])
                left = mid;
            else
                right = mid-1;
        }
        return nums[left];
    }
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值