C++ 面试高频考点 力扣 34. 在排序数组中查找元素的第一个和最后一个位置 二分查找左右端点 题解 每日一题

文章目录

  • 二分查找进阶,精准定位左右边界
  • 题目描述
  • 先踩坑:朴素二分为什么搞不定重复元素?
  • 第一步:找左边界——如何定位“第一个target”?
  • 第二步:找右边界——如何定位“最后一个target”?
  • 完整代码实现
    • 代码细节说明
    • 对比总结:三种二分模板的核心区别
    • 避坑指南:这些错误别再犯!
  • 没错最喜欢的模板
  • 模板本质复盘——为什么“二段性”是万能钥匙?
  • 总结:从“会用”到“活用”的3个步骤

这是封面原图,和AI弄的动图:
在这里插入图片描述
在这里插入图片描述

二分查找进阶,精准定位左右边界

上一篇博客C++ 面试高频考点 力扣 704.二分查找咱们用LeetCode 704题吃透了“朴素二分查找”,对付无重复元素的有序数组那是手到擒来。但要是数组里藏着重复元素,比如[1,2,2,2,3],想找2第一次和最后一次出现的位置,朴素二分就像“摸到鱼却抓不住首尾”——明明能找到2,却没法确定它是不是边界,最后只能靠遍历补漏,时间复杂度又退回O(n)

今天咱们就借着LeetCode 34题(在排序数组中查找元素的第一个和最后一个位置),拆解二分查找的“左右边界查找模板”。还是老规矩,从“为什么朴素二分不行”说起,再抓“二段性”这个核心,把边界查找的逻辑和细节扒得明明白白~

在这里插入图片描述

题目描述

题目链接:力扣 34. 在排序数组中查找元素的第一个和最后一个位置

题目描述:
给定一个按照升序排列的整数数组 ,和一个目标值 。找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 ,返回 。
你必须设计并实现时间复杂度为  的算法解决此问题。

示例 1:
输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]

示例 2:
输入:nums = [5,7,7,8,8,10], target = 6
输出:[-1,-1]

示例 3:
输入:nums = [], target = 0
输出:[-1,-1]

提示:

  • 0 <= nums.length <= 10^5
  • -10^9 <= nums[i] <= 10^9
  • nums 是一个非递减数组
  • -10^9 <= target <= 10^9

先踩坑:朴素二分为什么搞不定重复元素?

咱们先试试用昨天的朴素二分套这道题。比如数组nums = [1,2,2,2,3]target = 2

  1. 初始left=0right=4mid=2nums[mid]=2 == target,按朴素二分的逻辑,直接返回2——但这只是2的中间位置,不是我们要的“第一个”或“最后一个”;
  2. 要是想找第一个2,就得在找到mid=2后,继续往左找;想找最后一个2,就得继续往右找。可这样一来,就需要在二分结束后再遍历,最坏情况比如nums = [1,1,1,...,1](1万个1),target=1,遍历一遍又回到O(n),完全违背了二分O(log n)的初衷。

问题出在哪?昨天的朴素二分,把数组分成了“小于target”“等于target”“大于target”三部分,但面对重复元素时,“等于target”的部分是一个区间,我们需要的是这个区间的左右端点,而不是区间里的任意一个点。

所以,核心思路得变:不再找“等于target的点”,而是用“二段性”把数组分成“一定不包含左端点”和“可能包含左端点”两部分(找左边界时),或者“一定不包含右端点”和“可能包含右端点”两部分(找右边界时)。

第一步:找左边界——如何定位“第一个target”?

核心逻辑:重新定义“二段性”

找左边界的关键,是把数组分成两部分:

  • 左半部分:所有元素 < target(一定不包含左边界);
  • 右半部分:所有元素 ≥ target(可能包含左边界)。

比如nums = [1,2,3,3,3,4,5]target=3

  • 左半部分(< 3):[1,2]
  • 右半部分(≥ 3):[3,3,3,4,5]

我们要找的“左边界”,就是右半部分的第一个元素(下标2)。只要每次通过mid判断,把左半部分(<target)全部舍弃,最终剩下的那个点,就是左边界。

具体步骤拆解

  1. 初始化边界left=0right = nums.size()-1(和朴素二分一致,闭区间)。
  2. 循环条件left < right(重点!不是left <= right,后面解释)。
  3. 计算midmid = left + (right - left)/2(防溢出,且这里必须用“向下取整”,后面说原因)。
  4. 判断与缩范围
    • nums[mid] < targetmid在左半部分(一定不包含左边界),直接舍弃左半,left = mid + 1
    • nums[mid] ≥ targetmid在右半部分(可能包含左边界),不能舍弃mid,所以right = mid
  5. 循环结束后验证:此时left == right(因为循环条件是left < right,退出时必相等),但要确认这个点是不是target(比如数组全小于target时,left会指向最后一个元素,仍小于target)。

关键细节:为什么是left < right?为什么mid要向下取整?

咱们用三个场景验证,就能明白这些细节的必要性:

场景1:数组包含target(如nums=[1,2,3,3,3,4]target=3

  • 初始left=0right=5mid=2nums[2]=3 ≥3)→ right=2
  • 此时left=0 < right=2,继续循环,mid=0nums[0]=1 <3)→ left=1
  • 此时left=1 < right=2,继续循环,mid=1nums[1]=2 <3)→ left=2
  • 现在left=2 == right=2,循环退出。验证nums[2]=3 == target,左边界就是2。

场景2:数组全大于target(如nums=[4,5,6]target=3

  • 初始left=0right=2mid=1nums[1]=5 ≥3)→ right=1
  • 继续循环,mid=0nums[0]=4 ≥3)→ right=0
  • 退出循环,left=0 == right=0,但nums[0]=4≠3,返回-1。

场景3:数组全小于target(如nums=[1,2,3]target=4

  • 初始left=0right=2mid=1nums[1]=2 <4)→ left=2
  • 继续循环,mid=2nums[2]=3 <4)→ left=3
  • 退出循环,left=3 == right=3,但3超出数组下标(或nums[3]不存在),返回-1。

从这三个场景能看出:

  • 循环条件用left < right:是为了让循环在left == right时退出,避免死循环。如果用left <= right,当left==right时会再进一次循环,此时mid=left=right,若nums[mid]≥targetright=mid,导致无限循环。
  • mid必须向下取整(即left + (right-left)/2):如果用向上取整(left + (right-left+1)/2),比如场景1中最后一步left=1right=2,会算出mid=2nums[2]=3≥3)→ right=2,此时left=1 < right=2,下次循环还是mid=2,陷入死循环。

第二步:找右边界——如何定位“最后一个target”?

右边界的逻辑和左边界对称,但细节上有反转,别搞混!

核心逻辑:对称的“二段性”

找右边界的关键,是把数组分成两部分:

  • 左半部分:所有元素 ≤ target(可能包含右边界);
  • 右半部分:所有元素 > target(一定不包含右边界)。

还是用nums = [1,2,3,3,3,4,5]target=3

  • 左半部分(≤ 3):[1,2,3,3,3]
  • 右半部分(> 3):[4,5]

我们要找的“右边界”,就是左半部分的最后一个元素(下标4)。每次通过mid判断,把右半部分(>target)全部舍弃,最终剩下的点就是右边界。

具体步骤拆解

  1. 初始化边界left=0right = nums.size()-1(和左边界一致)。
  2. 循环条件left < right(和左边界一致)。
  3. 计算midmid = left + (right - left + 1)/2(重点!这里要用“向上取整”,和左边界相反)。
  4. 判断与缩范围
    • nums[mid] > targetmid在右半部分(一定不包含右边界),直接舍弃右半,right = mid - 1
    • nums[mid] ≤ targetmid在左半部分(可能包含右边界),不能舍弃mid,所以left = mid
  5. 循环结束后验证:此时left == right,同样要确认这个点是不是target

关键细节:为什么mid要向上取整?

还是用场景验证,比如nums=[1,2,3,3,3,4]target=3

  • 初始left=0right=5mid=0 + (5-0+1)/2=3nums[3]=3 ≤3)→ left=3
  • 此时left=3 < right=5,继续循环,mid=3 + (5-3+1)/2=4nums[4]=3 ≤3)→ left=4
  • 此时left=4 < right=5,继续循环,mid=4 + (5-4+1)/2=5nums[5]=4 >3)→ right=4
  • 退出循环,left=4 == right=4,验证nums[4]=3 == target,右边界就是4。

如果这里用向下取整(mid=left + (right-left)/2),比如最后一步left=4right=5,会算出mid=4nums[4]=3 ≤3)→ left=4,此时left=4 < right=5,下次循环还是mid=4,陷入死循环。

所以,右边界的mid必须向上取整,才能避免死循环——这是和左边界最核心的区别!

完整代码实现

结合左边界和右边界的逻辑,我们可以写出完整代码,注意处理“数组为空”“target不存在”等边缘情况:

class Solution {
public:vector<int> searchRange(vector<int>& nums, int target) {// 先处理特殊情况:数组为空,直接返回[-1,-1]if (nums.empty()) {return {-1, -1};}vector<int> result(2, -1); // 初始化结果为[-1,-1]int n = nums.size();int left = 0, right = n - 1;// 第一步:找左边界while (left < right) {// mid向下取整(左边界专用)int mid = left + (right - left) / 2;if (nums[mid] < target) {// 左半部分(<target)舍弃,left移到mid+1left = mid + 1;} else {// 右半部分(≥target)保留,right移到midright = mid;}}// 循环结束后,left==right,验证是否是targetif (nums[left] == target) {result[0] = left;} else {// 左边界都不是target,说明数组中没有target,直接返回[-1,-1]return result;}// 第二步:找右边界(左边界已确认存在,不用重新初始化left)right = n - 1; // 只需要重置rightwhile (left < right) {// mid向上取整(右边界专用)int mid = left + (right - left + 1) / 2;if (nums[mid] > target) {// 右半部分(>target)舍弃,right移到mid-1right = mid - 1;} else {// 左半部分(≤target)保留,left移到midleft = mid;}}// 右边界一定是target(因为左边界已存在)result[1] = right;return result;}
};

代码细节说明

  1. 数组为空处理:一开始就判断nums.empty(),避免后面访问nums[left]时报错。
  2. 左边界验证后直接返回:如果左边界不是target,说明数组中没有target,不用再找右边界,直接返回[-1,-1],提高效率。
  3. 右边界无需重置left:因为左边界已经是target的位置,右边界一定在左边界右侧,直接用左边界作为起点即可。

对比总结:三种二分模板的核心区别

为了帮大家理清思路,咱们把朴素二分、左边界二分、右边界二分的核心区别做成表格,方便对比记忆:

模板类型核心目标二段性划分循环条件mid计算方式(取整)缩范围逻辑(关键)结果验证
朴素二分找任意一个target<target / ==target / >targetleft ≤ right向下取整(防溢出)nums[mid]==target→返回;<target→left=mid+1;>target→right=mid-1循环内直接返回,没找到返回-1
左边界二分找第一个target<target / ≥targetleft < right向下取整<target→left=mid+1;≥target→right=mid循环后left==right,验证nums[left]==target
右边界二分找最后一个target≤target / >targetleft < right向上取整>target→right=mid-1;≤target→left=mid循环后left==right,验证nums[left]==target

记忆口诀

  • 左边界:找“第一个≥target”,mid向下取整,right=mid
  • 右边界:找“最后一个≤target”,mid向上取整,left=mid
  • 循环条件都是left < right,退出必相等,验证不可少。

避坑指南:这些错误别再犯!

  1. mid取整错误:左边界用向上取整、右边界用向下取整,必陷入死循环。
  2. 循环条件用left <= right:左边界/右边界模板中,会导致left==right时继续循环,进而死循环。
  3. 忘记验证结果:比如数组全小于target时,左边界会指向最后一个元素,但该元素≠target,直接返回会出错。
  4. 右边界重置left:找右边界时,left已经是左边界(target的位置),无需重置为0,否则会浪费时间。

没错最喜欢的模板

  1. 左端点
while(left < right)
{int mid = left + (right - left)/2;if(.....)//判断条件left = mid + 1;elseright = mid;
}
  1. 右端点
while(left < right)
{int mid = left + (right - left + 1)/2;if(.....)//判断条件left = mid;elseright = mid - 1;
}

如果下面出现-1的时候,上面就 +1

模板本质复盘——为什么“二段性”是万能钥匙?

看到这里,你可能会问:为什么不管是找左右边界,都能靠“二段性”解决?这就要回到二分查找的本质了。

二分查找的核心不是“找target”,而是“通过缩小范围,快速定位目标”,而“二段性”是实现这一目标的唯一前提——只要数组能被划分为“满足某条件”和“不满足某条件”的两部分,且两部分边界清晰,就能用二分查找。

我们再梳理三种模板的“二段性”本质:

模板类型二段性本质(核心条件)目标位置
朴素二分条件A:nums[mid] == target(唯一)满足条件A的位置
左边界二分条件A:nums[mid] ≥ target满足条件A的第一个位置
右边界二分条件A:nums[mid] ≤ target满足条件A的最后一个位置

所有二分变形题,本质都是“确定条件A”和“确定目标位置(第一个/最后一个满足A的位置)”。比如:

  • LeetCode 35(搜索插入位置):条件A是nums[mid] ≥ target,目标是第一个满足A的位置;
  • LeetCode 69(x的平方根):条件A是mid² ≤ x,目标是最后一个满足A的位置;
  • 甚至“找数组中唯一不重复的元素”“旋转数组的最小元素”等题,本质也是通过定义新的“条件A”,用二分缩小范围。

总结:从“会用”到“活用”的3个步骤

通过这篇博客,我们从“朴素二分的局限”出发,拆解了左右边界模板的逻辑,并用实战题验证了模板的通用性。最后,给大家总结一套“从会用到活用”的学习路径:

  1. 抓核心,记模板:先理解左右边界模板的“循环条件、mid取整、缩范围逻辑”,重点记住“左边界找第一个≥target,右边界找最后一个≤target”;
  2. 辨题型,定条件:遇到新题时,先思考“这道题要找的是‘第一个满足某条件的位置’还是‘最后一个满足某条件的位置’”,确定“条件A”是什么(比如LeetCode 69的条件A是mid² ≤x);
  3. 勤调试,避陷阱:写代码时,先测试小规模数组和边缘场景(空数组、target在首尾、target不存在),手动模拟循环过程,避免死循环和越界。

二分查找的难点不在代码本身,而在“如何将问题转化为边界查找”。只要掌握了“二段性”这个核心,不管是LeetCode的中等题,还是面试中的变形题,都能游刃有余。下次再遇到二分题,别慌,先问自己:“我要找的是左边界还是右边界?条件A是什么?”——想清楚这两个问题,代码自然就出来了!

在这里插入图片描述

“喏,Doro给你一朵小花🌸奖励看到这里的你,这篇二分查找的拆解有没有把你心里的‘小疑惑’全捋顺呀?要是你觉得这篇博客把单调性、二段性这些‘小细节’讲得明明白白,就给个点赞鼓励一下嘛~ 要是怕以后找不到这么贴心的讲解,可得赶紧收藏起来!不然下次遇到二分问题,Doro怕你会像Doro一样因为找不到 Orange 时那样‘委屈巴巴’哦~ Doro 知道这个博主后面还会扒更多算法‘小秘密’,关注他,带你从‘看着会’到‘写得对’,再也不被二分的细节‘背刺’啦~

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

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

相关文章

在word以及latex中引用zotero中的参考文献

背景 如何在word以及latex中引用zotero中的参考文献 历史参考 恢复Zotero软件内的误删条目数据/文献-CSDN博客使用zotero保存 CNKI知网文章时发生错误。改为尝试用 Save as Webpage 保存。-CSDN博客 word 在word中引用zotero中的参考文献 打开word&#xff0c;点击引用 经典…

docker 部署Skywalking

创建网络 docker network create skywalking-network docker compose 安装SkyWalking docker-compose.yaml 文件 version: "3" services:# SkyWalking OAP server with Elasticsearch storageskywalking-oap:image: apache/skywalking-oap-server:8.9.0container…

动态UI的秘诀:React中的条件渲染

动态UI的秘诀&#xff1a;React中的条件渲染 作者&#xff1a;码力无边各位React探险家&#xff0c;欢迎回到我们的《React奇妙之旅》&#xff01;我是你们的老朋友码力无边。在之前的旅程中&#xff0c;我们已经学会了如何创建组件、传递数据&#xff08;Props&#xff09;、管…

ubuntu挂载外接硬盘

查看找到硬盘sudo fdisk -l例如&#xff1a;名字为&#xff1a;/dev/sda创建挂载点sudo mkdir -p /2TSSD手动挂载&#xff08;单次生效&#xff0c;关机会失效&#xff09;sudo mount /dev/sda1 /2TSSD开机自动挂载&#xff08;永远生效&#xff0c;关机会失效&#xff09;S1&a…

数学思想 | 数学思维过程对象封装

注&#xff1a;本文为 “数学思维过程对象封装” 相关译文。 英文引文&#xff0c;机翻未校。 略作重排&#xff0c;如有内容异常&#xff0c;请看原文。 What is the object of the encapsulation of a process? 过程封装的对象是什么&#xff1f; David Tall#, Michael Th…

常见视频封装格式对比

一、核心概念&#xff1a;封装格式 vs 编码格式 编码格式 (Codec): 例如 H.264, H.265 (HEVC), AV1, VP9。它负责对原始视频和音频数据进行压缩&#xff0c;是决定视频体积和清晰度的关键。封装格式 (Container): 例如 MP4, MKV, AVI。它负责将已经压缩好的视频、音频、字幕等打…

Java实现PDF表格转换为CSV

在很多企业办公和数据分析的场景中&#xff0c;PDF 中常常存放着报表、清单或统计数据。相比 PDF&#xff0c;CSV 文件 更易于在 Excel 或数据库中进行进一步处理。因此&#xff0c;我们常常需要一种方式&#xff0c;将 PDF 中的表格数据批量抽取并导出为 CSV 文件。 本文将介…

具有类人先验知识的 Affordance-觉察机器人灵巧抓取

25年8月来自武汉大学、阿里达摩院、湖畔研究中心、浙大和清华的论文“Towards Affordance-Aware Robotic Dexterous Grasping with Human-like Priors”。 能够泛化抓取目标的灵巧手是开发通用具身人工智能的基础。然而&#xff0c;之前的方法仅仅关注低级抓取稳定性指标&#…

项目管理的关键成功因素

项目管理的关键成功因素包括&#xff1a;目标明确、科学规划、有效沟通、资源保障、风险管理、团队协作、持续监控与总结改进。目标明确保证方向不偏移、科学规划确保执行有章可循、有效沟通减少误解与冲突、资源保障提供坚实支撑、风险管理帮助预防问题、团队协作提升整体效率…

[光学原理与应用-338]:ZEMAX - Documents\Zemax\Samples

Documents\Zemax\Samples 是 Zemax OpticStudio 软件自带的样例文件目录&#xff0c;包含大量预设的光学设计案例&#xff0c;涵盖镜头设计、照明系统、公差分析、非序列光学等多个领域。这些样例是学习软件功能、验证设计方法和快速启动项目的宝贵资源。以下是该目录的详细解析…

el-table合并列实例

想要实现效果&#xff1a;目前接口返回数据data:[{companyCode: "NXKYS",companyName:1123,costContractId:1123,costContractName:1123,createBy:1123,details:[{brand:1123,contractItemName:1123,modelSpec:1123,projectItemId:1123,requestQty:1123,transactionZ…

虚假 TradingView Facebook 广告在全球传播 Android 间谍软件

一项快速发展的恶意广告活动最初通过 Meta 的广告网络针对 Windows 用户&#xff0c;现已将其范围扩展到 Android 设备&#xff0c;推广伪装成合法交易应用程序的 Brokewell 恶意软件的高级版本。 Bitdefender Labs 警告称&#xff0c;此次移动攻击活动目前已在全球范围内展开…

Android系统框架知识系列(十九):Android安全架构深度剖析 - 从内核到应用的全栈防护

​关键词​&#xff1a;安全启动链、应用沙箱、SELinux、硬件安全模块、权限控制、零信任架构一、Android安全架构的基本概念与背景1. 移动安全环境的特殊性Android作为全球最大的移动操作系统&#xff0c;面临着独特的安全挑战&#xff1a;​移动设备的安全威胁维度​&#xf…

智能消防栓闷盖终端:让城市消防管理更智慧高效

然而您是否知道&#xff0c;这些传统的消防栓常常面临非法开启、人为破坏、水压不足等管理难题&#xff1f;当火灾真正发生时&#xff0c;它们能否可靠地提供"救命水"&#xff1f;如今&#xff0c;随着智能消防栓闷盖终端的出现&#xff0c;这一切正在悄然改变。 智…

【系统架构设计(一)】系统工程与信息系统基础上:系统工程基础概念

文章目录一、系统工程的基本概念二、系统工程方法论1、霍尔三维结构&#xff1a;硬科学2、切克兰德方法&#xff1a;软科学思维3、其他三、系统工程生命周期管理1、生命周期阶段划分2、生命周期方法论系统工程与信息系统基础为复杂系统设计提供从思维方法到具体技术的全方位指导…

[p2p-Magnet] 队列与处理器 | DHT路由表

第6章&#xff1a;队列与处理器 在第5章&#xff1a;分类器中&#xff0c;我们了解了系统如何分析原始种子数据。但当系统突然发现数百万新种子时&#xff0c;如何高效处理这些海量任务&#xff1f;这就是队列与处理器系统的职责所在。 核心概念 任务队列 功能定位&#xf…

Spring JDBC 源码初探:异常处理体系

一、Spring JDBC 异常体系简介 当我们使用 Spring JDBC 进行数据访问时&#xff0c;大多数人关注的是 JdbcTemplate 如何简化数据库操作&#xff0c;却很少有人去深入理解异常体系。事实上&#xff0c;异常不仅仅是错误提示&#xff0c;它是系统健壮性、可维护性的重要一环。JD…

如何提高微型导轨的生产效率?

在精密机械制造领域&#xff0c;每一个细微的元件都可能成为决定产品性能和品质的关键因素。而微型导轨正是体型小、高精度优势&#xff0c;在精密制造领域得到广泛应用&#xff0c;它高效支撑着现代工业的生产方式和效率。那么&#xff0c;如何提高微型导轨的生产效率呢&#…

轻量xlsx读取库xlsx_drone的编译与测试

这个库是在看其他网页时&#xff0c;作为和功能丰富的xlsxio库的对比来的&#xff0c;按照xlsx_drone github页面介绍&#xff0c; 特征 不使用任何外部应用程序来解析它们。注重速度而不是功能。简单的接口。UTF-8 支持。 安装 直接将 src 和 ext 文件夹复制并粘贴到项目根文…

Linux/UNIX系统编程手册笔记:文件I/O、进程和内存分配

文件 I/O 深度解析&#xff1a;掌握通用 I/O 模型的核心逻辑 在 Linux 系统编程中&#xff0c;文件 I/O 是程序与外部设备&#xff08;文件、设备等 &#xff09;交互的基础。从打开文件到读写数据&#xff0c;再到关闭资源&#xff0c;一系列系统调用构成了通用 I/O 模型的核心…