leetcode34.排序数组中查找元素第一个和最后一个位置两种解题方法(超详细)

news2024/11/16 21:34:32

34. 在排序数组中查找元素的第一个和最后一个位置 - 力扣(LeetCode)icon-default.png?t=N7T8https://leetcode.cn/problems/find-first-and-last-position-of-element-in-sorted-array/description/?envType=list&envId=ZCa7r67M这道题,读者可能会说这道题有什么好讲的?

不就是双指针一次就出结果吗?

本题我们将以双指针和二分查找两种思路讲解,并且着重讲解二分查找的方法,和其中的代码实现细节,喜欢看代码细节的读者可以耐心看一看,相信会有一定的收获。

第一种解法是双指针

思路:由于数组有序,呈递增状态,我们要找的是给定目标值target,在该数组内部 出现的起始位置和终止位置,可以使用双指针从两边开始逐步探测,各指针都是只要找到target了,就停止寻找,直到两个位置都找到了target,就跳出,不用向里再寻找,此时的两指针指向位置正是答案,如果两指针指向一起也是正确的,因为可能target只出现了一次,如果循环结束还没找到结果(left<=right),那说明无法找到答案,返回{-1,-1}。

下面是可以通过的代码

class Solution {
public:
    vector<int> searchRange(vector<int>& nums, int target) {
        int l=0,r=nums.size()-1;
        while(l<=r){
            if(nums[l]==target&&nums[r]==target)return {l,r};
            if(nums[l]!=target)++l;
            if(nums[r]!=target)--r;
            
        }
        return {-1,-1};
    }
};

简洁的代码!你以为这就完了吗?判断的部分,也就是判断这两个位置对应元素是否都等于target这条代码一定要先写, 不能写在先更改l或者r的后面,看起来逻辑没有什么不同,怎么写都对,但是实际上是有代码错误的,比如这条测试用例nums:[1]    target: 0

虽然我们的判断逻辑是没问题的,但是先进行left和right的偏移,可能会使接下来的判断这两个位置对应元素是否都等于target这条代码出现越界错误。这是不容易被察觉到的错误,要小心。如果是先判断就不会有这样的错误,因为调整完left和right后,会进行的首先是越界判断,直接跳出循环了。

第二种解法二分查找

这一思路我用分别计算出左右边界的方法来讲解,这样写不容易出错,且思路清晰,代码有bug也更容易排查,强烈建议算法使用不是很熟练的读者使用这种写法。

解题思路就是分别求出左右边界,我讲其中之一,左边界,右边界与左边界思路是相同的,只是方向不同而已,稍加改动即可。

先给出代码根据代码进行分析

class Solution {
public:
    vector<int> searchRange(vector<int>& nums, int target) {
        int left=leftget(nums,target);
        int right=rightget(nums,target);
        if(left==-10||right==-10)return {-1,-1};
        if(right-left>1)return {left+1,right-1};
        return {-1,-1};
    }
    int leftget(vector<int>&nums,int target){
        int left=0,right=nums.size()-1;int res=-10;
        while(left<=right){
            int mid=(left+right)>>1;
            if(nums[mid]>=target){
                right=mid-1;res=right;
            }
            else left=mid+1;
        }
        return res;
    }
    int rightget(vector<int>&nums,int target){
        int left=0,right=nums.size()-1;int res=-10;
        while(left<=right){
            int mid=(left+right)>>1;
            if(nums[mid]<=target){
                left=mid+1;res=left;
            }
            else right=mid-1;
        }
        return res;
    }
};

细节非常多(只讨论left<=right)
把寻找左右界限分成了两个函数,然后求解
先看寻找边界的代码:
是用二分查找的方法写的,定义一个变量然后更新这个变量为返回值返回左右边界
寻找左边界,如果mid对应数据大于等于target将right左移,right=mid-1,大于target左移right天经地义,我们来看看等于时候
如果mid对应数据等于mid意味着什么?意味着我们要找的target范围数组就在此时的left——right之间,这个时候还左移right会使right最终移过target这片区域的最左端
正是这样,我们每一次进来,都更新左边界值为right。

几个疑问:
怎么确定right不断左移而最终会导致right走向target区域的左端点的?
其实整个过程不是通过一直的做right左移操作而使right一下子就确定了target区域的左端点的,而总是需要不停移left和right最终使right走向左端点
当mid对应数据大于等于target时候需要左移动right缩小范围,这时候target一定有全部或部分区域处在left——right这个范围内。
mid对应数据小于target也不一定就意味着target就一定不在范围内,可能是right移动完之后使得mid对应数据暂时小于target,这时候右移动left之后,可能会使mid对应数据重新大于等于target
如果是target不在left——right这种情境下,mid所取值一定是小于target了,所以left不停右移直到超出循环,而不会再更新最终target左窗口,也就是此时right的值。
上面的推论是保证right找到了左端点前一个位置时候,不会再乱动!

换句话说:当mid对应数据在target区间内时候,right每次更新为mid-1,也就是说只有两种可能right还是target,更新完right之后mid对应数据可能暂时性的小于targrt,但不要担心
我们调整left移动会最终使没走完的right继续左移动(这是mid由于right的缘故重新大于等于target)
如果上次mid指向的是起始点的target呢?那么right就指向了target区域的最左端点的左一个位置!!!这个时候mid一直会保持小于target
所以会一直右移动right,直到循环出去,不会更新数据,这也就是right为什么会保证一定处于target左端点前一个位置的重要原因!!
特例辩证:当right离边界很近的时候,left会不停缩减,直到仅剩一个元素,这个时候left和right所指那个元素一定是target,然后mid就是target,然后right向左移动
为什么一定指向right,假如left——right某时候只剩两个元素【0,1】而我们要找的是1,那么nums【0】==0会使left继续缩小范围
如果剩余的数据是【0,1,2】那也是一样的,right此时在0位置停下(说right离target边界近这个作为特例原因在于如果target离right远,比如在范围中间,那么肯定会正常的运行,如果很近那你可能不好想
为什么不会是right踩到边界然后退出循环,而是一定正好走到边界左一个位置?其实你举个例子就明白了)


为什么不先进行更新左边界再更新right?
因为没更新right之前,right此时的位置只能说明left——right中间的某位置确实是target所在的区域,而更新了right为mid-1,right此时就有可能作为target区域的左边界的左一个位置了
如果在不更新right时候去先更新最终答案,只会得到一个错误的答案,因为在最后一次左移动right数据时候,那个时候right可能处于target附近,那么这个时候就正好离我们要找的数据很近,
你可以调整返回值+1或者-1操作来通过一些用例,但是这并不能证明思路是正确的,这很可能是凑巧,并没什么逻辑性可言。
也很可能right离target很远,mid才是指向最后一个左端点位置,这个你找到的right一定是错误答案,这也就说明了如果你先更新最终要返回的边界数据,然后依赖于对这个数据+-1,调整
这是错误的想法,一开始我就对这个代码顺序进行了调整,因为我发现左右边界返回值始终都需要对左边界+1,右边界-1,所以萌生出先更新边界再更新right可能会得到简单的不需要后续处理的正确答案,
但实际上这种想法是十分愚蠢的。

左端点求解和右端点的求解很类似,它们是相同的思路,只不过方向不同而已,上面我们讲了左端点,就不再赘述右端点,类比一下就可以了
再来看有没有其他的问题,值得我们讲解。确实有一个而且是隐藏很深的问题。
在讲这个问题之前,先来看一些简单的,比如最后的处理数据方面,最后的处理数据返回答案方面,有三种情况需要讨论。
第一种是如果返回回来的值也就是左右边界其中之一为初始化时候的垃圾值,那么直接返回{-1,-1}这种情况是在说明target目标值范围处在整个给定数组的左边或者右边,简单地说就是出界了,
给定数组里不仅没有,而且也不可能有比如说给定数组是【1,2,3】而target为10,这种情况就是。
这种超边界的情况下,自己模拟一下代码就知道,求左边界时候,更新左边界的代码根本进不去,所以也就更新不了。它引出了我们后面要说的,为什么不把求解左右边界函数里的左右边界的初始化值
初始化为-1,这样不是更直观吗?这个我们放在后面讲解。

第二种情况是right-left>1才能进入真正的返回,也就是找到了答案子数组,有人会说这不对啊,为什么不是大于等于?这不会落下只有单独一个target的情况吗?
一开始我也是这么想,后来改代码测试一下,发现答案是错误的,
拿两种测试用例来分析
【1】【5,7,7,8,8,10】这两种,第一个target=1,第二个target=6,你会发现反倒是target=1这种看起来不可能过的用例通过了,但是其他的某些用例过不去。
这和返回值有关,你会发现它是先比较的左右边界返回值,而我们知道它的左右边界是要做处理的,比如【1】它的左右边界会返回【-1,1】而做了处理才会是真答案也就是【0,0】
所以不用担心这种target只有一个的,它也会正确输出,但是【5,7,7,8,8,10】target=6这种不一样,它代表了我们需要过滤出去的第二个错误情况,也就是给定数组的范围包含target,但实际上target不存在于
该数组中,这种情况会使得最终的left和right会缩在一起,因为它无法找到答案,但是target却确确实实的处在给定数组的范围内,以这个用例而言左边界的left和right会缩到1下标处也就对应数据7
然后mid过大,right移动向左,处在0这个位置上,跳出循环。而右边界是移动left然后取答案的,模拟可以知道,left和right都会处在7这个数据,然后right更新向左,右边界函数会返回此时left所在区域也就是1
这个下标,这样右边界减去左边界正好等于1,没错正好把错误的情况取答案了!所以不能写成大于等于1,而是大于1。
你可以这样想,如果左右边界取完差值等于1是什么情况(没做处理之前)?就是left和right在左右边界取时候,分别在没有找到target时候,交叉的越界,且距离为1。
这是典型的,target处在给定数组数据范围内,而不实际出现于给定数组中,请大家格外注意这种情况。
这种求解左右边界的代码如果遇到target只有1个的情况时候,会拉开距离返回去,也就是说,一定会在target下标的左右偏移1的位置出现,也就是说一定大于1,只有target有0个时候,才会拉不开距离相减等于1

第三种情况是直接返回{-1,-1}这种情况对应的其实就是情况二里找不到时候,应该返回的【-1,-1】因为上面我们分析的是为什么不写>=1,这个等于1就是错误答案,在后面返回-1就可以了
取到正确答案时候,在情况二中返回正确序列即可。

最后我们终于可以说到这个隐晦错误了,为什么左右边界函数边界返回值不能初始化为-1?
这个是由测试用例【1】这种类型引起的错误,当target只有1个,而且target在左边界时候,就会出现左边界取到-1,这个时候如果你的边界初始化就是-1,且以该值作为错误答案判断依据时候
就会发生错误,原本正确的解被丢弃。所以我们初始化应该取一个除了小于0且不等于-1的数,从这些里初始化值就正确了,

至此把所有疑点全部讲解完毕。

还有就是最后的判断部分读者可能感觉有点奇怪,考虑对返回答案做一些适当的调整,不过不要忘记对可能出错误的判断部分做出调整,主要是:对于target不在数组内部和target在数组范围但是不在数组里
这两种情况,做出调整后,题解依然正确。

给出一个更改后的示例代码:

class Solution {
public:
    vector<int> searchRange(vector<int>& nums, int target) {
        int left=leftget(nums,target);
        int right=rightget(nums,target);
        if(left==-9||right==-11)return {-1,-1};
        if(right-left>=0)return {left,right};
        return {-1,-1};
    }
    int leftget(vector<int>&nums,int target){
        int left=0,right=nums.size()-1;int res=-10;
        while(left<=right){
            int mid=(left+right)>>1;
            if(nums[mid]>=target){
                right=mid-1;res=right;
            }
            else left=mid+1;
        }
        return res+1;
    }
    int rightget(vector<int>&nums,int target){
        int left=0,right=nums.size()-1;int res=-10;
        while(left<=right){
            int mid=(left+right)>>1;
            if(nums[mid]<=target){
                left=mid+1;res=left;
            }
            else right=mid-1;
        }
        return res-1;
    }
};

本期内容就到这里

如果对您有用的话别忘了一键三连哦,如果是互粉回访我也会做的!

大家有什么想看的题解,或者想看的算法专栏、数据结构专栏,可以去看看往期的文章,有想看的新题目或者专栏也可以评论区写出来,讨论一番,本账号将持续更新。
期待您的关注

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1226983.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

二阶低通滤波器(二阶巴特沃斯滤波器)

连续传递函数G(s) 离散传递函数G(z) 差分方程形式 二阶巴特沃斯滤波器参数设计 设计采样频率100Hz&#xff0c;截止频率33Hz。 注意&#xff1a;设计参数使用在离散系统中&#xff01; 同理&#xff0c;其他不同阶数不同类型的滤波器设计&#xff0c;如二阶高通滤波器、二阶…

Transformer ZOO

Natural Language Processing Transformer:Attention is all you need URL(46589)2017.6 提出Attention机制可以替代卷积框架。引入Position Encoding&#xff0c;用来为序列添加前后文关系。注意力机制中包含了全局信息自注意力机制在建模序列数据中的长期依赖关系方面表现出…

嵌入式开发--赛普拉斯cypress的铁电存储器FM25CL64B

嵌入式开发–赛普拉斯cypress的铁电存储器FM25CL64B 简介 FM25CL64B是赛普拉斯cypress出品的一款铁电存储器&#xff0c;这种存储器最大的优势是可以像RAM一样随机存储&#xff0c;和按字节写入&#xff0c;也可以像ROM一样掉电仍然可以保存数据&#xff0c;是一种相当优秀的…

宠物信息服务预约小程序的效果如何

宠物的作用越来越重要&#xff0c;因此铲屎官们对自己爱宠的照顾也是加倍提升&#xff0c;而市场围绕宠物展开的细分服务近些年来逐渐增多&#xff0c;且市场规模快速增长。涉及之广&#xff0c;涵盖宠物衣食住行、医疗、美容、婚丧嫁娶等&#xff0c;各品牌争相抢夺客户及抢占…

java游戏制作-拼图游戏

一.制作主界面 首先创建一个Java项目命名为puzzlegame。 再在src中创建一个包&#xff0c;用来制作主界面 代码&#xff1a; 结果&#xff1a; 二.设置界面 代码&#xff1a; 三.初始化界面 代码&#xff1a; 优化代码&#xff1a; 结果&#xff1a; 四.添加图片 先在Java项…

思维模型 留白效应

本系列文章 主要是 分享 思维模型 &#xff0c;涉及各个领域&#xff0c;重在提升认知。因留白而遐想。 1 留白效应的应用 1.1 留白效应在艺术领域的应用 欧洲的艺术和设计领域有很多经典的实际案例&#xff0c;其中荷兰画家文森特梵高的作品《星夜》是一幅非常著名的油画&am…

【沐风老师】3DMAX一键云生成器插件使用教程

3DMAX云生成器插件使用教程 3DMAX云生成器插件&#xff0c;是一款将物体变成云的简单而富有创意的工具。该工具通过在物体周围创建粒子结合材质&#xff0c;最终形成渲染后的云的效果。 【支持版本】 3dMax2018 – 2023 默认的扫描线渲染器 【安装方法】 1.复制“安装文件”…

4、FFmpeg命令行操作10

音视频处理流程 先看两条命令 ffmpeg -i test_1920x1080.mp4 -acodec copy -vcodec libx264 -s 1280x720 test_1280x720.flv ffmpeg -i test_1920x1080.mp4 -acodec copy -vcodec libx265 -s 1280x720 test_1280x720.mkv ffmpeg音视频处理流程

Mysql之单行函数

Mysql之单行函数 单行函数数值类型函数字符串类型的函数日期和时间函数加密与解密函数信息函数 单行函数 函数的定义 函数在计算机语言的使用中贯穿始终&#xff0c;函数的作用是什么呢&#xff1f;它可以把我们经常使用的代码封装起来&#xff0c; 需要的时候直接调用即可。这…

Hive 定义变量 变量赋值 引用变量

Hive 定义变量 变量赋值 引用变量 变量 hive 中变量和属性命名空间 命名空间权限描述hivevar读写用户自定义变量hiveconf读写hive相关配置属性system读写java定义额配置属性env只读shell环境定义的环境变量 语法 Java对这个除env命名空间内容具有可读可写权利&#xff1b; …

MySQL 的执行原理(三)

5.4. InnoDB 中的统计数据 我们前边唠叨查询成本的时候经常用到一些统计数据&#xff0c;比如通过 SHOW TABLE STATUS 可以看到关于表的统计数据&#xff0c;通过 SHOW INDEX 可以看到关于索引 的统计数据&#xff0c;那么这些统计数据是怎么来的呢&#xff1f;它们是以什么方…

4种经典的限流算法

0、基础知识 1000毫秒内&#xff0c;允许2个请求&#xff0c;其他请求全部拒绝。 不拒绝就可能往db打请求&#xff0c;把db干爆~ interval 1000 rate 2&#xff1b; 一、固定窗口限流 固定窗口限流算法&#xff08;Fixed Window Rate Limiting Algorithm&#xff09;是…

pm2在Windows环境中的使用

pm2 进程管理工具可以Windows操作系统上运行&#xff0c;当一台Windows电脑上需要运行多个进程时&#xff0c;或者运维时需要运行多个进程以提供服务时。可以使用pm2&#xff0c;而不再是使用脚本。 1. 使用PM2管理进程 1.1. 启动PM2项目 1.1.1. 直接启动项目 参数说明&…

c++ list容器使用详解

list容器概念 list是一个双向链表容器&#xff0c;可高效地进行插入删除元素。 List 特点&#xff1a; list不可以随机存取元素&#xff0c;所以不支持at.(position)函数与[]操作符。可以对其迭代器执行&#xff0c;但是不能这样操作迭代器&#xff1a;it3使用时包含 #includ…

C++ 运算符重载详解

本篇内容来源于对c课堂上学习内容的记录 通过定义函数实现任意数据类型的运算 假设我们定义了一个复数类&#xff0c;想要实现两个复数的相加肯定不能直接使用“”运算符&#xff0c;我们可以通过自定义一个函数来实现这个功能&#xff1a; #include <iostream> using…

RabbitMQ消息的可靠性

RabbitMQ消息的可靠性 一 生产者的可靠性 生产者重试 有时候由于网络问题&#xff0c;会出现连接MQ失败的情况&#xff0c;可以配置重连机制 注意&#xff1a;SpringAMQP的重试机制是阻塞式的&#xff0c;重试等待的时候&#xff0c;当前线程会等待。 spring:rabbitmq:conne…

MySQL 的执行原理(四)

5.5. MySQL 的查询重写规则 对于一些执行起来十分耗费性能的语句&#xff0c;MySQL 还是依据一些规则&#xff0c;竭尽全力的把这个很糟糕的语句转换成某种可以比较高效执行的形式&#xff0c;这个过程也可以 被称作查询重写。 5.5.1. 条件化简 我们编写的查询语句的搜索条件…

【STM32】ADC(模拟/数字转换)

一、ADC的简介 1.什么是ADC 1&#xff09;将【电信号】-->【电压】-->【数字量】 2&#xff09;ADC可以将引脚上连续变化的模拟电压转换为内存中存储的数字量&#xff0c;建立模拟电路到数字电路的桥梁。 3&#xff09;12位逐次逼近型ADC&#xff0c;1us转换时间&#xf…

iOS_折叠展开 FoldTextView

1. 显示效果 Test1&#xff1a;直接使用&#xff1a; Test2&#xff1a;在 cell 里使用&#xff1a; 2. 使用 2.1 直接使用 // 1.1 init view private lazy var mooFoldTextView: MOOFoldTextView {let view MOOFoldTextView(frame: .zero)view.backgroundColor .cyanvie…