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

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.hqwc.cn/news/194380.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

系列十一、你平时工作用过的JVM常用基本配置参数有哪些?

一、常用参数 1.1、-Xms 功能&#xff1a;初始内存大小&#xff0c;默认为物理内存的1/64&#xff0c;等价于 -XX:InitialHeapSize 1.2、-Xmx 功能&#xff1a;最大分配内存&#xff0c;默认为物理内存的1/4&#xff0c;等价于 -XX:MaxHeapSize 1.3、-Xss 功能&#xff1a;设置…

C++--哈希表--散列--冲突--哈希闭散列模拟实现

文章目录 哈希概念一、哈希表闭散列的模拟实现二、开散列(哈希桶)的模拟实现数据类型定义析构函数插入查找删除 哈希概念 unordered系列的关联式容器之所以效率比较高&#xff0c;是因为其底层使用了哈希结构。 顺序结构以及平衡树中&#xff0c;元素关键码与其存储位置之间没…

HTML5+CSS3+JS小实例:使用L2Dwidget实现二次元卡通看板娘

实例:使用L2Dwidget实现二次元卡通看板娘 技术栈:HTML+CSS+JS 效果: 源码: <!DOCTYPE html> <html><head><meta http-equiv="content-type" content="text/html; charset=utf-8"><meta name="viewport" conte…

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

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

4种经典的限流算法

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

​软考-高级-系统架构设计师教程(清华第2版)【第14章 云原生架构设计理论与实践(P496~526)-思维导图】​

软考-高级-系统架构设计师教程&#xff08;清华第2版&#xff09;【第14章 云原生架构设计理论与实践&#xff08;P496~526&#xff09;-思维导图】 课本里章节里所有蓝色字体的思维导图

利用SD存储介质扩展MAXQ20000的非易失性数据存储空间

SD存储卡是一种可移动存储介质&#xff0c;通常用于相机、手机、平板电脑等设备中存储照片、视频、音乐等数据。SD存储卡的全称为Secure Digital Memory Card&#xff0c;是由SD Card Association制定的一种标准格式。它具有体积小、存储容量大、读写速度快、价格低廉等优点。目…

java游戏制作-拼图游戏

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

三相异步电机动态数学模型及矢量控制仿真

文章目录 三相异步电机动态数学模型及矢量控制仿真1、异步电机三相方程2、坐标变换3、磁链3/2变换推导4、两相静止坐标系下的方程5、两相旋转坐标系下的方程6、以 ω-is-Ψr 为状态变量的状态方程7、矢量控制及 matlab 仿真 原文链接需要仿真的同学请关注【Qin的学习营地】 三相…

c++ list容器使用详解

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

AcWing 3. 完全背包问题 学习笔记

有 N&#xfffd; 种物品和一个容量是 V&#xfffd; 的背包&#xff0c;每种物品都有无限件可用。 第 i&#xfffd; 种物品的体积是 vi&#xfffd;&#xfffd;&#xff0c;价值是 wi&#xfffd;&#xfffd;。 求解将哪些物品装入背包&#xff0c;可使这些物品的总体积不…

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…