标签搜索

目 录CONTENT

文章目录

旋转排序数组系列题详解.md

小小城
2021-08-22 / 0 评论 / 0 点赞 / 3 阅读 / 4,953 字 / 正在检测是否收录...
温馨提示:
本文最后更新于 2022-05-02,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

旋转排序数组系列题详解

@[toc]

一、问题描述:旋转数组的最小数字

在这里插入图片描述

二、分析:二分查找

一个包含重复元素的升序数组在经过旋转之后,可以得到下面可视化的折线图:
在这里插入图片描述

  •  其中横轴表示数组元素的下标,纵轴表示数组元素的值。图中标出了最小值的位置,是我们需要旋转的目标。

  •  我们考虑数组中的最后一个元素 x在最小值右侧的元素,它们的值一定都小于等于 x;而在最小值左侧的元素,它们的值一定都大于等于 x。因此,我们可以根据这一条性质,通过二分查找的方法找出最小值。

  •  在二分查找的每一步中,左边界为 low,右边界为high,区间的中点为 pivot,最小值就在该区间内。我们将中轴元素numbers[pivot] 与右边界元素 numbers[high] 进行比较,可能会有以下的三种情况

  • 第一种情况是numbers[pivot]<numbers[high]

如下图所示,这说明numbers[pivot] 是最小值右侧的元素,因此我们可以忽略二分查找区间的右半部分。
在这里插入图片描述

  • 第二种情况是numbers[pivot]>numbers[high]

如下图所示,这说明 numbers[pivot] 是最小值左侧的元素,因此我们可以忽略二分查找区间的左半部分。
在这里插入图片描述

  • 第三种情况是numbers[pivot]==numbers[high]。

如下图所示,由于重复元素的存在,我们并不能确定 numbers[pivot] 究竟在最小值的左侧还是右侧,因此我们不能莽撞地忽略某一部分的元素。

我们唯一可以知道的是,由于它们的值相同,所以无论numbers[high] 是不是最小值,都有一个它的「替代品numbers[pivot],因此我们可以忽略二分查找区间的右端点。
在这里插入图片描述

  • 当二分查找结束时,我们就得到了最小值所在的位置。

三、代码

class Solution {
public:
    int minArray(vector<int>& numbers) {
        int low = 0;
        int high = numbers.size() - 1;
        while (low < high) 
        {
            int pivot = low + (high - low) / 2;
            if (numbers[pivot] < numbers[high]) 
            {
                high = pivot;
            }
            else if (numbers[pivot] > numbers[high]) 
            {
                low = pivot + 1;
            }
            else 
            {
                high -= 1;
            }
        }
        return numbers[low];
    }
};

四、问题描述:寻找旋转排序数组中的最小值

在这里插入图片描述

五、分析:二分搜索

一种暴力的解法是搜索整个数组,找到其中的最小元素,这样的时间复杂度是 O(N)其中 N 是给定数组的大小。

  •  一个非常棒的解决该问题的办法是使用二分搜索。在二分搜索中,我们找到区间的中间点并根据某些条件决定去区间左半部分还是右半部分搜索。

  •  由于给定的数组是有序的,我们就可以使用二分搜索。然而,数组被旋转了,所以简单的使用二分搜索并不可行。

  •  在这个问题中,我们使用一种改进的二分搜索,判断条件与标准的二分搜索有些不同。

  •  我们希望找到旋转排序数组的最小值,如果数组没有被旋转呢?如何检验这一点呢?

  •  如果数组没有被旋转,是升序排列,就满足 last element > first element。

在这里插入图片描述

  • 上图例子中 7>2 。说明数组仍然是有序的,没有被旋转。
    在这里插入图片描述

  • 上面的例子中 3 < 4,因此数组旋转过了。这是因为原先的数组为 [2, 3, 4, 5, 6, 7],通过旋转较小的元素 [2, 3] 移到了后面,也就是 [4, 5, 6, 7, 2, 3]。因此旋转数组中第一个元素 [4] 变得比最后一个元素大

  • 这意味着在数组中你会发现一个变化的点,这个点会帮助我们解决这个问题,我们称其为变化点。

在这里插入图片描述

  •  在这个改进版本的二分搜索算法中,我们需要找到这个点。下面是关于变化点的特点:

  • 所有变化点左侧元素 > 数组第一个元素

  • 所有变化点右侧元素 < 数组第一个元素

  •  算法

  • 找到数组的中间元素 mid。

  • 如果中间元素 > 数组第一个元素,我们需要在 mid 右边搜索变化点。

  • 如果中间元素 < 数组第一个元素,我们需要在 mid 做边搜索变化点。

在这里插入图片描述

  • 上面的例子中,中间元素 6 比第一个元素 4 大,因此在中间点右侧继续搜索。

  •  当我们找到变化点时停止搜索,当以下条件满足任意一个即可:

  • nums[mid] > nums[mid + 1],因此 mid+1 是最小值

  • nums[mid - 1] > nums[mid],因此 mid 是最小值

在这里插入图片描述

  • 在上面的例子中,标记左右区间端点。中间元素为 2,之后的元素是 7 满足 7 > 2 也就是 nums[mid - 1] > nums[mid]。因此找到变化点也就是最小元素为 2。

六、代码

class Solution {
public:
    int findMin(vector<int>& nums) {
        if (nums.size() == 1) 
        {
            return nums[0];
        }

        int left = 0, right = nums.size() - 1;
        if (nums[right] > nums[0]) 
        {
            return nums[0];
        }

        while (right >= left) 
        {
      
            int mid = left + (right - left) / 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;
    }
};

七、问题描述:寻找旋转排序数组中的最小值 II

在这里插入图片描述

八、分析:二分查找

  •  一个包含重复元素的升序数组在经过旋转之后,可以得到下面可视化的折线图:
    在这里插入图片描述

  •  其中横轴表示数组元素的下标,纵轴表示数组元素的值。图中标出了最小值的位置,是我们需要旋转的目标。

  •  我们考虑数组中的最后一个元素 x:在最小值右侧的元素,它们的值一定都小于等于 x;而在最小值左侧的元素,它们的值一定都大于等于 x。因此,我们可以根据这一条性质,通过二分查找的方法找出最小值。

  •  在二分查找的每一步中,左边界为low,右边界为high,区间的中点为pivot,最小值就在该区间内。

  •  我们将中轴元素nums[pivot] 与右边界元素nums[high] 进行比较,可能会有以下的三种情况:

  • 第一种情况是nums[pivot]<nums[high]。

如下图所示,这说明nums[pivot] 是最小值右侧的元素,因此我们可以忽略二分查找区间的右半部分。
在这里插入图片描述

  • 第二种情况是nums[pivot]>nums[high]。

如下图所示,这说明 nums[pivot] 是最小值左侧的元素,因此我们可以忽略二分查找区间的左半部分。
在这里插入图片描述

  • 第三种情况是 nums[pivot]==nums[high]。

如下图所示,由于重复元素的存在,我们并不能确定nums[pivot] 究竟在最小值的左侧还是右侧,因此我们不能莽撞地忽略某一部分的元素。

我们唯一可以知道的是,由于它们的值相同,所以无论 nums[high] 是不是最小值,都有一个它的「替代品」nums[pivot],因此我们可以忽略二分查找区间的右端点。

在这里插入图片描述

  • 当二分查找结束时,我们就得到了最小值所在的位置。

九、代码

class Solution {
public:
    int findMin(vector<int>& nums) {
        int low = 0;
        int high = nums.size() - 1;
        while (low < high) 
        {
            int pivot = low + (high - low) / 2;
            if (nums[pivot] < nums[high]) 
            {
                high = pivot;
            }
            else if (nums[pivot] > nums[high]) 
            {
                low = pivot + 1;
            }
            else 
            {
                high -= 1;
            }
        }
        return nums[low];
    }
};

十、问题描述:搜索旋转排序数组

在这里插入图片描述

十一、分析:二分搜索

题目要求算法时间复杂度必须是 $O(logn)$ 的级别,这提示我们可以使用二分搜索的方法。

  •  但是数组本身不是有序的,进行旋转后只保证了数组的局部是有序的,这还能进行二分搜索吗?答案是可以的。
  •  可以发现的是,我们将数组从中间分开成左右两部分的时候,一定有一部分的数组是有序的。
  • 拿示例来看,我们从 6 这个位置分开以后数组变成了 [4, 5, 6] 和 [7, 0, 1, 2] 两个部分,其中左边 [4, 5, 6]这个部分的数组是有序的,其他也是如此。
  •  这启示我们可以在常规二分搜索的时候查看当前 mid 为分割位置分割出来的两个部分 [l, mid] 和 [mid + 1, r]
  •  看哪个部分是有序的,并根据有序的那个部分确定我们该如何改变二分搜索的上下界,因为我们能够根据有序的那部分判断出 target 在不在这个部分:
  • 如果 [l, mid - 1] 是有序数组,且 target 的大小满足 [nums[l],nums[mid]),则我们应该将搜索范围缩小至 [l, mid - 1],否则在 [mid + 1, r] 中寻找。
  • 如果 [mid, r] 是有序数组,且 target 的大小满足(nums[mid+1],nums[r]],则我们应该将搜索范围缩小至 [mid + 1, r],否则在 [l, mid - 1] 中寻找。

在这里插入图片描述

  • 需要注意的是,二分的写法有很多种,所以在判断 target 大小与有序部分的关系的时候可能会出现细节上的差别。

十二、代码

class Solution {
public:
    int search(vector<int>& nums, int target) {
        int n = (int)nums.size();
        
        if (!n) 
            return -1;
        
        if (n == 1) 
            return nums[0] == target ? 0 : -1;
        
        //左右边界
        int l = 0, r = n - 1;
        while (l <= r) 
        {
        	//中间的值
            int mid = (l + r) >> 1;

			//相等直接返回
            if (nums[mid] == target) 
                return mid;
            
            //代表【l,mid】是有序的
            if (nums[0] <= nums[mid]) 
            {
            	//如果目标值target满足【nums[0],nums[mid])代表在
            	//左半区间查找
                if (nums[0] <= target && target < nums[mid]) 
                {
                    r = mid - 1;
                } 
                //反之右半区间查找
                else 
                {
                    l = mid + 1;
                }
            } 
            //代表(mid,r】是有序的
            else 
            {
            	//在右半区间查找
                if (nums[mid] < target && target <= nums[n - 1]) 
                {
                    l = mid + 1;
                } 
                //在左半区间查找
                else 
                {
                    r = mid - 1;
                }
            }
        }
        return -1;
    }
};

十三、问题描述:搜索旋转排序数组II

在这里插入图片描述

十四、代码

class Solution {
public:
    bool search(vector<int>& nums, int target) {
        int left = 0,right = nums.size() - 1;
        
        while(left <= right)
        {
            while(left != right && nums[left] == nums[right]) 
            	right--; //无重复值的解法中添加这行
            
            int mid = (left + right) / 2;
            if(nums[mid] == target) 
            	return true;
            else if(nums[mid] > target)
            {
                if(nums[mid] > nums[right] && target < nums[left]) 	
                	left = mid + 1;
                else 
                	right = mid - 1;
            }
            else
            {
                if(nums[mid] < nums[left] && target > nums[right]) right=mid-1;
                else 
                	left = mid + 1;
            }
        }
        return false;
    }
};

0

评论区