搜索旋转排序数组[特殊二分]

搜索旋转排序数组[特殊二分]

一、LeetCode 33. 搜索旋转排序数组

整数数组 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、旋转的本质

简单来说,旋转就是将一个原本升序的数组,从某一个点pivot断开,将后半部分整体移至前面:

image-202209221759370582、思路分析

首先,通过O(n)的暴力遍历的方式是最简单易理解的,但是没有利用排序数组这一条件。一般来说,看到排序搜索,直接想到的就是二分查找。如何进行二分呢?

常规的二分

对于常规的二分查找,我们通过计算mid,将数组二分成左右两个升序数组,从而推算left与right指针的移动方式。

特殊的二分

本题我们同样可以通过mid把数组分成左右两个部分,但是特殊的地方在于:原本升序的数组从pivot处旋转成了两个升序数组,左边是升序的,然后出现一个跳崖,再继续升序。因此本题要使用二分解决,需要对mid的位置进行分类讨论**[记数组为nums]**:

  1. mid在前半部分的升序数组中
    • 如果nums[mid]>target,此时按道理应该将right指针向小的地方移动,但是这里可以看到,nums[mid]左边的值是小于nums[mid]的,后半部分数组也是小于nums[mid]的,这时候需要target的介入:
      • 如果target>=nums[0],即target也在前半部分,那么正常将right指针移动至mid-1
      • 如果target<nums[0],即target在后半部分,那么此时向后半部分搜索,因此需要将left指针移动至mid+1
    • 如果nums[mid]<target,此时按道理应该将left指针向大的地方移动。只有前半部分数组中大于mid的部分满足要求,因此将left指针移动至mid+1

image-20220922195239142

当数组进行旋转后,原先pivot处变成了新数组的0下标,后半部分的升序数组全部小于nums[0]。因此,当nums[mid]>nums[0]时,即可断定mid处在新数组的前半部分。

  1. mid在后半部分的升序数组中

    • 如果nums[mid]>target,此时按道理应该将right指针向小的地方移动,这里比nums[mid]小的只有后半部分数组中mid左边的部分了,因此将right指针移动至mid-1
    • 如果nums[mid]<target,此时按道理应该将left指针向大的地方移动。但是这里可以看到,nums[mid]右边的值是大于nums[mid]的,前半部分数组也是大于nums[mid]的,这时候需要target的介入:
      • 如果target>=nums[0],即target在前半部分,那么此时向前半部分搜索,因此需要将right指针移动至mid-1
      • 如果target<nums[0],即target在后半部分,那么正常将left指针移动至mid+1

    image-20220922200654182

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

二、LeetCode 81. 搜索旋转排序数组 II

已知存在一个按非降序排列的整数数组 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,4,4,5,6,6,7] 在下标 5 处经旋转后可能变为 [4,5,6,6,7,0,1,2,4,4] 。

给你 旋转后 的数组 nums 和一个整数 target ,请你编写一个函数来判断给定的目标值是否存在于数组中。如果 nums 中存在这个目标值 target ,则返回 true ,否则返回 false 。

本题与上题相比只有一个改动:元素有重复

上一题由于没有重复元素,因此可以简单地通过nums[mid]与nums[0]的关系判断mid在前半部分还是后半部分,但是本题不能这么做,假设有以下两种情况:

image-20220922204300864

image-20220922204435048

同样是nums[mid]>=nums[0],但是图1的mid在前半部分,图2的mid在后半部分。究其原因,是因为nums[l]==nums[r]。在这种情况下,旋转数组的转折点变得不再清晰。

为了避免这种不清晰的情况,我们在nums[l]==nums[mid] && nums[mid]==nums[r] && nums[mid]!=target时,选择将l指针向右移,r指针向左移,通过这种最朴素的方式来缩小二分区间。

「这种方法在极端条件,比如数组元素全部是1,而target!=1时,时间复杂度退化至O(n)」

当然,我们还可以直接通过预处理,选择一个新的start作为二分查找的起点,其中nums[start]!=nums[n-1],从而使start成为新的判断mid在前半部分还是后半部分的标准。

当然,由于这种方法一次只会将指针移动一次,因此效率上比前一种慢一倍。

除去这一特殊情况,其它的判断方式与上一题完全相同。

bool search(vector<int>& nums, int target) 
{
    int l = 0;
    int r = nums.size() - 1;

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

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

已知一个长度为 n 的数组,预先按照升序排列,经由 1 到 n 次 旋转 后,得到输入数组。例如,原数组 nums = [0,1,2,4,5,6,7] 在变化后可能得到:
若旋转 4 次,则可以得到 [4,5,6,7,0,1,2]
若旋转 7 次,则可以得到 [0,1,2,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 ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素

这题再次与之前的题目不一样,是要找出数组的最小元素,也就是后半部分数组的第一个元素

由于不存在重复元素,因此可以通过nums[mid]与nums[0]的关系判断left与right的移动方向:

  • 如果nums[mid]>nums[n-1],说明mid在数组的前半部分,而我们要找的是后半部分的第一个元素,因此将left移动至mid+1

    注意:当旋转数组依然是升序的,比如[0,1,2,3,4],那么通过nums[mid]>=nums[0]判断mid在前半部分就会出错。因此,最好的判断mid位置的方式是将nums[mid]与nums[n-1]比较

  • 如果nums[mid]<=nums[n-1],说明mid在数组的后半部分,我们需要继续向左搜索以定位至第一个元素,但是mid可能是第一个元素,所以right只能移动至mid

int findMin(vector<int>& nums) 
{
    int l = 0;
    int r = nums.size() - 1;
    while (l < r)
    {
        int mid = l + ((r - l) >> 1);
        if (nums[mid] <= nums[n - 1]) // 在旋转点的右半部分
            r = mid;
        else
            l = mid + 1;
    }
    return nums[l];
}

四、LeetCode 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 ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。

本题在上题的基础上,增设了元素可重复的条件。

因此,我们仿照**[二、LeetCode 81. 搜索旋转排序数组 II]**的做法,在mid位置无法判断时暴力缩小区间即可。

int findMin(vector<int>& nums) 
{
    int n = nums.size();
    int l = 0;
    int r = n - 1;
    while (l < r)
    {
        int mid = l + ((r - l) >> 1);
        if (nums[mid] == nums[l] && nums[mid] == nums[r])
        {
            --r; 
            ++l;
        }
        else if (nums[mid] <= nums[r]) // 后半部分
        {
            r = mid;
        }
        else // 前半部分
        {
            l = mid + 1;
        }
    }
    return nums[l];
}
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

白龙码~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值