IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 数据结构与算法 -> 搜索旋转排序数组[特殊二分] -> 正文阅读

[数据结构与算法]搜索旋转排序数组[特殊二分]

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

一、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];
}
  数据结构与算法 最新文章
【力扣106】 从中序与后续遍历序列构造二叉
leetcode 322 零钱兑换
哈希的应用:海量数据处理
动态规划|最短Hamilton路径
华为机试_HJ41 称砝码【中等】【menset】【
【C与数据结构】——寒假提高每日练习Day1
基础算法——堆排序
2023王道数据结构线性表--单链表课后习题部
LeetCode 之 反转链表的一部分
【题解】lintcode必刷50题<有效的括号序列
上一篇文章      下一篇文章      查看所有文章
加:2022-09-24 21:19:25  更:2022-09-24 21:22:37 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/19 18:11:55-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码