LeetCode - 34. 在排序数组中查找元素的第一个和最后一个位置

题目

34. 在排序数组中查找元素的第一个和最后一个位置 - 力扣(LeetCode)

思路

查找左边界

初始化 left = 0, right = nums.size() - 1

当 left < right 时循环:

  • 计算中点 mid = left + (right - left) / 2
  • 如果 nums[mid] < target,目标在右侧,left = mid + 1
  • 否则,目标在左侧或当前位置,right = mid

循环结束后,left == right,检查 nums[left] 是否等于 target

查找右边界

初始化 left = 0, right = nums.size() - 1

当 left < right 时循环:

  • 计算中点 mid = left + (right - left + 1) / 2 (向上取整)
  • 如果 nums[mid] > target,目标在左侧,right = mid - 1
  • 否则,目标在右侧或当前位置,left = mid

循环结束后,left == right,检查 nums[left] 是否等于 target

图解过程

以数组 [5, 7, 7, 8, 8, 10],目标值 target = 8 为例:

查找左边界

初始状态:

索引:  0  1  2  3  4  5
数组: [5, 7, 7, 8, 8, 10]↑           ↑left        right

 第一次迭代:

  • mid = 0 + (5-0)/2 = 2
  • nums[mid] = 7 < 8,目标在右侧
  • left = mid + 1 = 3
索引:  0  1  2  3  4  5
数组: [5, 7, 7, 8, 8, 10]↑     ↑left  right

 第二次迭代:

  • mid = 3 + (5-3)/2 = 4
  • nums[mid] = 8 == 8,目标可能在左侧
  • right = mid = 4
索引:  0  1  2  3  4  5
数组: [5, 7, 7, 8, 8, 10]↑  ↑left right

 第三次迭代:

  • mid = 3 + (4-3)/2 = 3
  • nums[mid] = 8 == 8,目标可能在左侧
  • right = mid = 3
索引:  0  1  2  3  4  5
数组: [5, 7, 7, 8, 8, 10]↑leftright

 循环结束:

  • left == right = 3,循环结束
  • nums[left] = 8 == target,找到左边界 begin = 3

查找右边界

初始状态:

索引:  0  1  2  3  4  5
数组: [5, 7, 7, 8, 8, 10]↑           ↑left        right

 第一次迭代:

  • mid = 0 + (5-0+1)/2 = 3 (向上取整)
  • nums[mid] = 8 == 8,目标可能在右侧
  • left = mid = 3
索引:  0  1  2  3  4  5
数组: [5, 7, 7, 8, 8, 10]↑     ↑left  right

 第二次迭代:

  • mid = 3 + (5-3+1)/2 = 5 (向上取整)
  • nums[mid] = 10 > 8,目标在左侧
  • right = mid - 1 = 4
索引:  0  1  2  3  4  5
数组: [5, 7, 7, 8, 8, 10]↑  ↑left right

 第三次迭代:

  • mid = 3 + (4-3+1)/2 = 4 (向上取整)
  • nums[mid] = 8 == 8,目标可能在右侧
  • left = mid = 4
索引:  0  1  2  3  4  5
数组: [5, 7, 7, 8, 8, 10]↑leftright

循环结束:

  • left == right = 4,循环结束
  • nums[left] = 8 == target,找到右边界 end = 4

最终结果

返回 [begin, end] = [3, 4],表示目标值 8 的第一个位置是索引 3,最后一个位置是索引 4。

关键点

  • 使用 left < right 作为循环条件,确保循环结束时 left == right
  • 查找左边界时使用向下取整计算 mid,更新方式为 right = mid
  • 查找右边界时使用向上取整计算 mid,更新方式为 left = mid
  • 循环结束后检查 nums[left] 是否等于目标值

读者可能会出现的错误写法

class Solution {
public:vector<int> searchRange(vector<int>& nums, int target) {if(nums.empty()){return {-1,-1};}int left = 0;int right = nums.size()-1;int begin = -1;int end =-1;while(left <= right){int mid = left + (right-left)/2;if(nums[mid]<target){left = mid + 1;}else{right = mid-1;}}if(nums[left] == target){begin = left;}left = 0;right = nums.size()-1;while(left <= right){int mid = left + (right-left)/2;if(nums[mid] > target){right = mid-1;}else{left = mid+1;}}if(nums[right] == target){end = right;}return {begin,end};}
};

在查找左边界的时候,right = mid而不是right = mid-1;在查找右边界的时候,left = mid而不是left = mid+1,并且left<right 而不是left<=right,并且当left=mid的条件下,mid = left + (right-left+1)/2,而不是mid = left + (right-left)/2

为什么要用right = mid而不是right = mid - 1?

  • 如果nums[mid] == target,mid可能就是我们要找的左端点,或者左端点在mid左侧
  • 如果我们使用right = mid - 1,就可能错过正确答案
  • 使用right = mid可以保留mid作为可能的答案,同时继续向左搜索

为什么我们使用right = mid - 1,就可能错过正确答案

假设数组中有多个相同的目标值,例如[1, 3, 3, 3, 5],目标值target = 3。 

当我们找到一个nums[mid] == target时(例如中间的那个3),我们不知道这是不是第一个出现的3。有两种可能:

  • 当前找到的这个3就是第一个3
  • 在mid左侧还有其他的3

使用right = mid - 1的问题:

如果当前找到的nums[mid] == target恰好是第一个出现的目标值,那么使用right = mid - 1会将搜索范围缩小到mid左侧,完全排除了mid这个位置。

具体例子:

  • 数组[1, 2, 3, 4, 5],target = 3
  • 初始:left = 0, right = 4
  • 第一次迭代:mid = 2, nums[mid] = 3 == target
  • 如果使用right = mid - 1,区间变为[0, 1]
  • 但索引2处的元素(值为3)是我们要找的答案!我们已经排除了它
  • 后续迭代会在[0, 1]范围内搜索,但这个范围内没有值为3的元素
  • 最终会得出错误结论,认为数组中不存在目标值

使用right = mid的优势:

当nums[mid] == target时,使用right = mid:

  • 将右边界移动到mid(而不是mid-1)
  • 保留了mid作为可能的答案
  • 同时继续在左侧搜索是否有更早出现的目标值

这样,即使当前的mid就是第一个目标值,我们也不会错过它,因为:

  • 如果左侧没有其他目标值,循环最终会收敛到这个位置
  • 如果左侧有目标值,我们会找到那个更早的位置

总结:使用right = mid - 1在找到目标值时会完全排除当前位置,如果当前位置恰好是第一个目标值,就会错过正确答案。而right = mid保留了当前位置作为候选答案,同时继续向左搜索可能存在的更早出现的目标值。

为啥查找左端点的mid算法和查找右端点的mid算法不一样

考虑当搜索区间缩小到只有两个元素时,例如 [3, 4]:

  • left = 3, right = 4
  • mid = 3 + (4-3)/2 = 3(向下取整)
  • 如果 nums[mid] <= target,执行 left = mid = 3
  • 新的搜索区间仍然是 [3, 4]
  • 下一次迭代,mid仍然是3
  • 如果 nums[mid] <= target,又执行 left = mid = 3
  • 搜索区间永远是 [3, 4],无法进一步缩小,陷入死循环

使用向上取整的解决方案

使用向上取整 mid = left + (right - left + 1) / 2 可以解决这个问题:

  • left = 3, right = 4
  • mid = 3 + (4-3+1)/2 = 4(向上取整)
  • 如果 nums[mid] > target,执行 right = mid - 1 = 3
  • 搜索区间变为 [3, 3],循环结束

或者,如果 nums[mid] <= target,执行 left = mid = 4,区间变为 [4, 4],循环也会结束。

为什么查找左端点不需要向上取整

关键在于更新策略的不同:

查找左端点时:

  • 当 nums[mid] < target 时,使用 left = mid + 1
  • 这个 +1 确保了即使 mid = left,区间也会缩小

查找右端点时:

  • 当 nums[mid] <= target 时,使用 left = mid(没有 +1)
  • 如果 mid = left,区间不会缩小,导致死循环

总结

  • 查找左端点时,即使使用向下取整,也不会导致死循环,因为:
  • 当 nums[mid] < target 时,left = mid + 1 会缩小区间
  • 当 nums[mid] >= target 时,right = mid 会缩小区间(因为 mid ≤ right)
  • 查找右端点时,使用向下取整可能导致死循环,因为:
  • 当 nums[mid] <= target 且 mid = left 时,left = mid 不会缩小区间

当使用right = mid和left = mid这种更新方式时,循环条件应该是while(left < right)而不是while(left <= right)。为啥呀

原因如下:

主要原因:避免死循环

如果使用while(left <= right)作为循环条件,当left == right时循环仍会继续执行:

当left == right时,无论使用什么mid计算方式,都会得到mid == left == right

此时:

  • 如果执行right = mid,结果是right = left,区间不变
  • 如果执行left = mid,结果是left = right,区间不变

下一次循环,条件left <= right仍然满足(因为left == right)

区间没有缩小,算法陷入死循环

具体例子

假设数组[1, 3, 5],当搜索区间缩小到[1, 1](即left = right = 1):

如果使用while(left <= right):

  • mid = 1
  • 如果执行right = mid,区间仍为[1, 1]
  • 如果执行left = mid,区间仍为[1, 1]
  • 下一次循环,条件仍然满足,陷入死循环

正确的写法

class Solution {
public:vector<int> searchRange(vector<int>& nums, int target) {if(nums.empty()){return {-1,-1};}int left = 0;int right = nums.size()-1;int begin = -1;int end =-1;while(left < right){int mid = left + (right-left)/2;if(nums[mid]<target){left = mid + 1;}else{right = mid;}}if(nums[left] == target){begin = left;}left = 0;right = nums.size()-1;while(left < right){int mid = left + (right-left + 1)/2;if(nums[mid] > target){right = mid-1;}else{left = mid;}}if(nums[right] == target){end = right;}return {begin,end};}
};

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

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

相关文章

Tesollo四指灵巧手DG-4F:18自由度与多种抓取模式结合实现高精度操作

Tesollo四指灵巧手 DG-4F 是一款具备 18 自由度的多模态末端执行器&#xff0c;采用模块化结构设计&#xff0c;融合人手灵活性与夹爪高效性特点。该产品兼容 Universal Robots、Techman、Doosan Robotics、Rainbow Robotics 等主流机器人平台&#xff0c;适用于工业自动化、科…

深入浅出JavaScript 原型链:对象继承的“隐形链条”

深入浅出JavaScript 原型链&#xff1a;对象继承的“隐形链条” 在 JavaScript 的世界里&#xff0c;原型链&#xff08;Prototype Chain&#xff09;是一个核心概念。它如同一条隐形的链条&#xff0c;连接着所有对象&#xff0c;使得代码能够高效地共享属性和方法。理解原型…

LINUX中MYSQL的使用

LINUX中MYSQL的使用 MYSQL的数据类型 bool&#xff1a; 布尔类型 0 或者 1 CHAR&#xff1a; 单字符的字符 CHAR&#xff08;n&#xff09;:多字节字符 VARCHAR&#xff08;n&#xff09;&#xff1a;可变长度的字符型 TINYINT &#xff1a; 单字节整型 SMALLINT&#x…

打卡第48天:随机函数与广播机制

知识点回顾&#xff1a; 随机张量的生成&#xff1a;torch.randn函数卷积和池化的计算公式&#xff08;可以不掌握&#xff0c;会自动计算的&#xff09;pytorch的广播机制&#xff1a;加法和乘法的广播机制 ps&#xff1a;numpy运算也有类似的广播机制&#xff0c;基本一致 …

学习昇腾开发的第四天--基本指令

1、查看npu当前状态信息 npu-smi info 2、查看NPU的ID npu-smi info -l3、调用python python3 4、修改用户名 su - HwHiAiUser 5、查看cann版本 cat /usr/local/Ascend/ascend-toolkit/latest/compiler/version.info 6、删除文件夹 sudo rm -rf HelloWorld7、在本地环…

vue3 - 自定义hook

自定义hook 简单点来说就是将人物或者订单的所有数据和方法放在一个ts文件里面 这样便于维护 假如一个人只需要管 人物的模块 那他只需要操作usePerson.ts文件就可以了 //useDog.ts import { ref,reactive} from vue; import axios from axios;export default function(){…

【python】bash: !‘: event not found

报错 # 2. 测试smplx是否工作&#xff08;可能不需要chumpy&#xff09; python -c "import smplx; print(✅ smplx works!)"bash: !: event not found 分析 这是bash的历史扩展问题&#xff0c;感叹号被解释为历史命令。用这些方法解决&#xff1a; &#x1f680…

【Python打卡Day47】注意力热力图可视化@浙大疏锦行

可视化空间注意力热力图的意义&#xff1a; 提升模型可解释性 热力图能直观展示模型决策的依据区域&#xff0c;破除深度学习"黑箱"困境。例如在图像识别中&#xff0c;可以看到模型识别"猫"是因为关注了猫耳和胡须区域&#xff0c;识别"禁止通行&qu…

树状数组 2

L - 树状数组 2 洛谷 - P3368 Description 如题&#xff0c;已知一个数列&#xff0c;你需要进行下面两种操作&#xff1a; 将某区间每一个数加上 x&#xff1b; 求出某一个数的值。 Input 第一行包含两个整数 N、M&#xff0c;分别表示该数列数字的个数和操作的总个数。…

YOLOv2 技术详解:目标检测的又一次飞跃

&#x1f9e0; YOLOv2 技术详解&#xff1a;目标检测的又一次飞跃 一、前言 在 YOLOv1 提出后&#xff0c;虽然实现了“实时性 单阶段”的突破&#xff0c;但其在精度和小物体检测方面仍有明显不足。为了弥补这些缺陷&#xff0c;Joseph Redmon 等人在 2017 年提出了 YOLOv2…

JAFAR Jack up Any Feature at Any Resolution

GitHub PaPer JAFAR: Jack up Any Feature at Any Resolution 摘要 基础视觉编码器已成为各种密集视觉任务的核心组件。然而&#xff0c;它们的低分辨率空间特征输出需要特征上采样以产生下游任务所需的高分辨率模式。在这项工作中&#xff0c;我们介绍了 JAFAR——一种轻量级…

SamWaf 开源轻量级网站防火墙源码(源码下载)

SamWaf网站防火墙是一款适用于小公司、工作室和个人网站的开源轻量级网站防火墙&#xff0c;完全私有化部署&#xff0c;数据加密且仅保存本地&#xff0c;一键启动&#xff0c;支持Linux&#xff0c;Windows 64位,Arm64。 主要功能&#xff1a; 代码完全开源 支持私有化部署…

79Qt窗口_QDockWidget的基本使用

目录 4.1 浮动窗⼝的创建 4.2 设置停靠的位置 浮动窗⼝ 在 Qt 中&#xff0c;浮动窗⼝也称之为铆接部件。浮动窗⼝是通过 QDockWidget类 来实现浮动的功能。浮动窗 ⼝⼀般是位于核⼼部件的周围&#xff0c;可以有多个。 4.1 浮动窗⼝的创建 浮动窗⼝的创建是通过 QDockWidget…

UE/Unity/Webgl云渲染推流网址,如何与外部网页嵌套和交互?

需求分析&#xff1a;用threejs开发的数字孪生模型&#xff0c; 但是通过webgl技术网页中使用&#xff0c;因为模型数据量大&#xff0c;加载比较慢&#xff0c;且需要和其他的业务系统进行网页嵌套和交互&#xff0c;使用云渲染技术形成的推流网址&#xff0c;如何与外部网页嵌…

在Termux中搭建完整Python环境(Ubuntu+Miniconda)

蹲坑也能写python? 📱 环境准备🛠 详细搭建步骤步骤1:安装Linux容器工具步骤2:查看可用Linux发行版步骤3:安装Ubuntu系统步骤4:登录Ubuntu环境步骤5:下载Miniconda安装包步骤6:安装Miniconda⚡ 环境验证💡 使用技巧⚠️ 注意事项前言:想在吃饭、通勤甚至休息间隙…

EventSourcing.NetCore:基于事件溯源模式的 .NET Core 库

在现代软件架构中&#xff0c;事件溯源&#xff08;Event Sourcing&#xff09;已经成为一种非常流行的模式&#xff0c;尤其适用于需要高可用性和数据一致性的场景。EventSourcing.NetCore 是一个基于事件溯源模式的 .NET Core 库&#xff0c;旨在帮助开发者更加高效地实现这一…

Linux下的第一个程序——进度条(命令行版本)

文章目录 编写Linux下的第一个小程序——进度条进度条的样式前置知识回车和换行缓冲区对回车、换行、缓冲区、输出的测试代码简单的测试样例倒计时程序 进度条程序理论版本基本框架代码实现 真实版本基础框架 代码实现 编写Linux下的第一个小程序——进度条 在前面的基础开发工…

【项目】仿muduo库one thread one loop式并发服务器前置知识准备

&#x1f4da; 博主的专栏 &#x1f427; Linux | &#x1f5a5;️ C | &#x1f4ca; 数据结构 | &#x1f4a1;C 算法 | &#x1f152; C 语言 | &#x1f310; 计算机网络 |&#x1f5c3;️ mysql 本文介绍了一种基于muduo库实现的主从Reactor模型高并发服务器框架…

steam报网络错误,但电脑是网络连接的

steam报网络错误&#xff0c;但电脑是网络连接的 如&#xff1a; 解决办法&#xff1a; 关闭电脑防火墙和所有杀毒软件&#xff0c;然后重新打开steam开代理&#xff0c;可能国内有时候访问不了 首选1进行尝试 steam安装路径一定要在纯英文路径下 已ok

Vue 组合式 API 与 选项式 API 全面对比教程

一、前言&#xff1a;Vue 的两种 API 风格 Vue 提供了两种编写组件逻辑的方式&#xff1a;组合式 API (Composition API) 和 选项式 API (Options API)。理解这两种方式的区别和适用场景&#xff0c;对于 Vue 开发者至关重要。 为什么会有两种 API&#xff1f; 选项式 API&a…