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

Posted 白龙码~

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了搜索旋转排序数组[特殊二分]相关的知识,希望对你有一定的参考价值。

文章目录

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

一、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断开,将后半部分整体移至前面:

2、思路分析

首先,通过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

当数组进行旋转后,原先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

    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在前半部分还是后半部分,但是本题不能这么做,假设有以下两种情况:

同样是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];

以上是关于搜索旋转排序数组[特殊二分]的主要内容,如果未能解决你的问题,请参考以下文章

二分法05:搜索旋转排序数组

[LeetCode]33. 搜索旋转排序数组(二分)

leetcode(33)---搜索旋转排序数组(二分查找)

二分查找(通过相对位置判断区间位置)--17--二分--LeetCode33搜索旋转排序数组

[LeetCode] 33. 搜索旋转排序数组 ☆☆☆(二分查找)

力扣 33. 搜索旋转排序数组 [二分]