当前位置: 首页 > news >正文

LeetCode 34. 在排序数组中查找元素的第一个和最后一个位置:二分查找实战

刷题路上,二分查找是绕不开的经典算法,而LeetCode 34题「在排序数组中查找元素的第一个和最后一个位置」,正是二分查找的进阶应用——它不仅要求我们找到目标值,更要精准定位其在非递减数组中的起始和结束位置,同时还要满足O(log n)的时间复杂度要求。今天就来拆解这道题,从题干分析到代码实现,再到细节坑点,一步步搞懂如何高效解决这道题。

一、题干解析:明确需求与约束

先再仔细读一遍题干,避免遗漏关键信息:

  • 输入:非递减顺序排列的整数数组nums,目标值target;

  • 输出:target在数组中的开始位置和结束位置,若不存在则返回[-1, -1];

  • 核心约束:必须设计时间复杂度为O(log n)的算法。

这里有两个关键点需要注意:

  1. 非递减数组:数组元素可能重复,且从左到右不递减(允许相等),这是二分查找的前提,也是我们定位边界的核心依据;

  2. O(log n)时间复杂度:直接遍历数组(O(n))会超时,因此必须用二分查找,且需要两次二分——分别找左边界(第一个等于target的位置)和右边界(最后一个等于target的位置)。

二、解题思路:两次二分查找,定位左右边界

既然数组是有序的,我们可以利用二分查找的特性,通过调整判断条件,分别找到target的左边界和右边界:

  1. 左边界:第一个大于等于target的位置(如果target存在,这个位置就是第一个target的索引;如果不存在,这个位置会大于数组长度或对应元素不等于target);

  2. 右边界:第一个大于target的位置,再减1(如果target存在,减1后就是最后一个target的索引;如果不存在,减1后会小于左边界)。

为了复用代码,我们可以设计一个通用的二分查找函数,通过一个布尔参数lower来控制查找左边界还是右边界:

  • 当lower为true时,查找左边界(第一个>=target的位置);

  • 当lower为false时,查找右边界的“临界位置”(第一个>target的位置)。

最后,通过判断左边界是否小于等于右边界、且边界对应的元素是否为target,来确定最终结果——如果满足,则返回[左边界, 右边界];否则返回[-1, -1]。

三、代码实现与逐行解析

先给出完整代码(TypeScript版本),再逐行拆解核心逻辑,确保每一步都能理解:

functionsearchRange(nums:number[],target:number):number[]{constn=nums.length;// 通用二分查找函数:lower控制查找左边界/右边界临界值constbinarySearch=(target:number,lower:boolean)=>{letleft=0,right=n-1,ans=n;// ans初始化为n,应对target大于所有元素的情况while(left<=right){constmid=Math.floor((left+right)/2);// 中间位置,避免溢出// 关键判断:根据lower调整二分方向if(nums[mid]>target||(lower&&nums[mid]>=target)){right=mid-1;// 目标在左半区,收缩右边界ans=mid;// 记录当前mid,可能是我们要找的边界}else{left=mid+1;// 目标在右半区,收缩左边界}}returnans;}letres:number[]=[-1,-1];// 初始化为不存在的情况constleftIdx=binarySearch(target,true);// 查找左边界constrightIdx=binarySearch(target,false)-1;// 查找右边界并调整// 验证边界的合法性:左边界<=右边界,且边界元素等于targetif(leftIdx<=rightIdx&&rightIdx<nums.length&&nums[leftIdx]===target&&nums[rightIdx]===target){res=[leftIdx,rightIdx];}returnres;};

核心代码逐行解析

1. 初始化与通用二分函数定义

const n = nums.length; —— 记录数组长度,避免多次调用nums.length,提升效率。

binarySearch函数:接收target和lower两个参数,返回对应边界的索引。这里ans初始化为n,是为了应对target大于数组中所有元素的情况(此时二分结束后,ans仍为n,后续rightIdx = n-1,会小于leftIdx,直接返回[-1,-1])。

2. 二分查找的核心判断逻辑

if (nums[mid] > target || (lower && nums[mid] >= target)):这是整个算法的灵魂,分两种情况理解:

  • 当lower为true(找左边界):只要nums[mid] >= target,就说明左边界可能在mid或mid左侧,因此收缩右边界(right = mid - 1),并记录当前mid为候选边界(ans = mid);

  • 当lower为false(找右边界临界值):只有nums[mid] > target时,才说明右边界临界值在mid或mid左侧,收缩右边界,否则继续向右查找。

else { left = mid + 1; }:当不满足上述条件时,说明目标在mid右侧,收缩左边界,继续查找。

3. 边界验证与结果返回

leftIdx = binarySearch(target, true):得到左边界(第一个>=target的位置);

rightIdx = binarySearch(target, false) - 1:得到第一个>target的位置,减1后就是最后一个<=target的位置(即右边界);

边界验证条件:leftIdx <= rightIdx(确保边界有效,不会出现左边界在右边界右侧的情况)、rightIdx < nums.length(避免数组越界)、nums[leftIdx] === target和nums[rightIdx] === target(确保找到的边界确实是target的位置,而非其他值的边界)。

四、关键坑点与注意事项

这道题看似简单,但很多人在二分查找的边界处理上容易出错,总结几个高频坑点:

  1. ans的初始值:必须设为n,而不是-1。如果target大于数组中所有元素,二分结束后left会超过right,ans仍为n,此时rightIdx = n-1,leftIdx = n,leftIdx > rightIdx,直接返回[-1,-1],避免出错;

  2. 二分循环条件:必须是left <= right,而不是left < right。如果用left < right,可能会错过最后一个符合条件的元素(比如数组中只有一个target时);

  3. 边界验证:不能只判断leftIdx <= rightIdx,还要验证nums[leftIdx]和nums[rightIdx]是否等于target。比如数组为[1,2,3,4],target为5,此时leftIdx = 4,rightIdx = 3,不满足leftIdx <= rightIdx;但如果target为0,leftIdx = 0,rightIdx = -1,也不满足;如果数组为[2,2],target为3,leftIdx = 2,rightIdx = 1,同样不满足;

  4. 整数溢出:mid的计算用Math.floor((left + right) / 2),在JavaScript/TypeScript中,整数范围足够大,不会出现溢出,但如果是其他语言(如Java),建议用left + Math.floor((right - left)/2),避免left+right溢出。

五、总结与拓展

这道题的核心是「二分查找的边界定位」,通过一次二分查找函数的复用,分别找到左、右边界,既满足了O(log n)的时间复杂度,又简化了代码逻辑。

拓展思考:

  • 如果数组是递减的,如何修改代码?只需调整二分查找的判断条件,将nums[mid] > target改为nums[mid] < target即可;

  • 如果题目要求找到target的出现次数,只需用右边界 - 左边界 + 1(若存在target),否则为0;

  • 二分查找的核心是「收缩边界」,只要明确“目标在左半区还是右半区”,就能灵活调整判断条件,解决各类边界查找问题。

http://www.jsqmd.com/news/536526/

相关文章:

  • 认知雷达前沿技术 量子力学基础
  • SpringBoot 编写第一个 REST 接口(Get/Post/Put/Delete)
  • 前后端分离校运会管理系统系统|SpringBoot+Vue+MyBatis+MySQL完整源码+部署教程
  • 2026年浙江旧木方优质供应商推荐榜:回收二手木方/回收二手模板/回收旧木方/回收旧模板木方/地坪保护橡胶垫租赁/选择指南 - 优质品牌商家
  • 【仅限首批订阅者】Python AOT编译性能天花板在哪?我们用SPEC CPU 2017 + 自研Python基准套件跑满72小时,结果颠覆认知…
  • OpenClaw安全指南:GLM-4.7-Flash环境下的权限控制与风险规避
  • OpenClaw+百川2-13B自动化内容处理:从网页抓取到Markdown生成
  • OpenClaw隐私保护模式:Qwen3-32B-Chat镜像敏感信息过滤实战
  • OpenClaw+百川2-13B:5个提升个人效率的自动化脚本实例
  • BGP路由优化:配置、故障排除与网络性能提升
  • 计算机毕业设计 java 装饰公司网站设计与实现 SpringBoot 装饰公司数字化展示与服务平台 JavaWeb 装饰设计与订单管理系统
  • 为什么“写入数据库”在生产环境中远比想象中复杂
  • 基于Python的私房菜定制上门服务系统毕业设计
  • 运维转行到网安,我后悔了?后悔没早转
  • 暗黑破坏神:技术焕新与经典重构——DevilutionX的跨平台复兴之路
  • SpringBoot 应用优雅停机:正确关闭服务的 3 种方式
  • Java学习笔记_Day14
  • ChatGPT模型排名实战指南:如何选择最适合业务场景的AI模型
  • 开源项目依赖管理:从架构设计到实战落地
  • DNS负载均衡:架构、优化与故障排查指南
  • 百川2-13B模型微调指南:提升OpenClaw自动化任务准确率
  • 木马与恶意软件深度实战:查杀原理 + 免杀对抗全攻略(2026 珍藏版)
  • 2026制造业机房报废设备回收厂家排行榜:机房存储设备回收/机房旧设备回收/机房服务器回收/机房机柜回收/机房淘汰设备回收/选择指南 - 优质品牌商家
  • 嵌入式NMEA-0183零内存分配解析器设计与实现
  • 如何快速构建轻量Windows 11系统:tiny11builder完整指南
  • Qwen3-4B模型微调指南:提升OpenClaw任务准确率
  • 自动机:创意编码动画引擎的终极实现方案
  • 中文语义相似度计算新范式:技术演进与实践路径
  • ChatGPT工作原理简述:从Transformer到AI辅助开发的实践指南
  • 嵌入式Linux多线程资源占用排查方法