前端进阶之旅前端进阶之旅
  • 基础篇HTML/CSS/JS 打底
  • 进阶篇原理与工程化
  • 高频篇面试最常问的那批
  • 精选篇按模块收敛的总结
  • 手写篇常考代码手写实现
  • 面经篇真实面试问题复盘
  • AI 篇NEWAI 时代的前端考点
  • 历年面经NEW按年份追踪真实考点
  • 每日一题每天一道,攒手感
  • 专项自测100 题快速查漏
  • 小程序题库小程序专项刷题
  • 算法题库NEW在线编码即时判题
  • 知识卡片NEW碎片时间过考点
  • 面试题大全常见问题解析
  • AI 答疑NEW随时提问,即时解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • AI 定制路线NEW按你的简历现排
  • AI 知识地图NEW串起全站知识点
  • 原理篇React / Vue 源码拆解
  • HTTP从报文一路讲到 HTTPS
  • 浏览器渲染、事件循环、进程
  • 计算机基础Linux、网络、操作系统
  • 设计模式23 种模式怎么用
  • Node学习指南从环境搭建到服务端
  • NPM工作流script、依赖与发布
  • Docker容器化部署上手
  • Canvas图形与动画实战
  • 前端系统进阶学习大型项目工程化
  • 前端综合文章长期沉淀的实践文
  • 思维导图知识点全景图
  • 学习路线按图索骥不跑偏
  • AI 热点NEWAI 每日动态
  • 公众号动态公众号历史文章
  • 博客动态站长的技术博客
  • 开发者导航常用工具与文档站
  • 基础篇HTML/CSS/JS 打底
  • 进阶篇原理与工程化
  • 高频篇面试最常问的那批
  • 精选篇按模块收敛的总结
  • 手写篇常考代码手写实现
  • 面经篇真实面试问题复盘
  • AI 篇NEWAI 时代的前端考点
  • 历年面经NEW按年份追踪真实考点
  • 每日一题每天一道,攒手感
  • 专项自测100 题快速查漏
  • 小程序题库小程序专项刷题
  • 算法题库NEW在线编码即时判题
  • 知识卡片NEW碎片时间过考点
  • 面试题大全常见问题解析
  • AI 答疑NEW随时提问,即时解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • AI 定制路线NEW按你的简历现排
  • AI 知识地图NEW串起全站知识点
  • 原理篇React / Vue 源码拆解
  • HTTP从报文一路讲到 HTTPS
  • 浏览器渲染、事件循环、进程
  • 计算机基础Linux、网络、操作系统
  • 设计模式23 种模式怎么用
  • Node学习指南从环境搭建到服务端
  • NPM工作流script、依赖与发布
  • Docker容器化部署上手
  • Canvas图形与动画实战
  • 前端系统进阶学习大型项目工程化
  • 前端综合文章长期沉淀的实践文
  • 思维导图知识点全景图
  • 学习路线按图索骥不跑偏
  • AI 热点NEWAI 每日动态
  • 公众号动态公众号历史文章
  • 博客动态站长的技术博客
  • 开发者导航常用工具与文档站
首页程序员面试题库二分查找 最后一个出现位置 upper_bound
算法算法搜索与排序

二分查找如何定位目标值的最后一个出现位置?

要定位目标值的最后一个出现位置,可先查找第一个大于它的索引,再将下标回退一步;这一策略让循环条件简单且不遗漏重复元素。

前端进阶之旅 · 一题精讲更新于 2026.09.05
算法#搜索与排序
先看核心答案读代码示例
理解线索

查找首个更大者后判前

  1. 收缩规则命中等于目标时,只移动左界向右推进
  2. 终止条件left突破到大于目标的第一个索引,循环就会停
  3. 回退校验返回left-1前必须确认目标存在

空数组或目标小于最小/大于最大时,left会成为0或n,回退前必须用条件挡住。

核心回答

先记住这个答案

使用左闭右开区间[0,n),在循环中若nums[mid] <= target则将左界移动到mid+1,否则把右界收到mid。结束时左界指向第一个大于target的位置,因此请检查left-1是否越界且等于target,是则返回该下标,否则返回-1。该写法同样说明二分为什么能直接应用在重复元素上,因为任何等于target的元素都被归入左侧推进通道。

  • 上界下标减一即潜在答案
  • 等于目标时左界必须前进以向右
  • 每次验证left-1是否等于目标

以上界为桥的实现逻辑

先定义区间为左闭右开[0,n),保持不变量 nums[0..left-1] <= target(若存在)且 nums[right..n-1] > target。循环时取中位索引 mid,检查 nums[mid] <= target 是否成立。成立说明 mid 左侧包括 mid 都不可能成为“第一个大于 target”的位置,所以把 left 推到 mid+1;否则当前 mid 位置已经超过 target,需要把右边界收到 mid。每步都削减查找量,直到 left == right,此时 left 正是第一个严格大于 target 的索引。

相对找第一个出现位置,这里把“相等”归入左侧推进,让相等的元素都被跳过,所以收敛点自然在重复段之后,因此结果是这段的右侧后一位置。随后用 left - 1 回退就能命中最后一个等于 target 的值。如果有多个不相等的值夹着,这个推断同样成立;如果数组中根本没出现 target,left - 1 可能指向比 target 更小的值,所以必须做一次相等校验。

findLastIndex 完整示例JavaScript
function findLast(nums, t) {
  let left = 0, right = nums.length;
  while (left < right) {
    let mid = left + Math.floor((right - left) / 2);
    if (nums[mid] <= t) {
      left = mid + 1;
    } else {
      right = mid;
    }
  }
  return left > 0 && nums[left - 1] === t ? left - 1 : -1;
}

console.log(findLast([1,2,2,2,3],2));    // 3
console.log(findLast([1,2,3,3,3,4],3));  // 4
console.log(findLast([5],5));            // 0
console.log(findLast([5],6));            // -1
console.log(findLast([],1));             // -1
console.log(findLast([2,2,2],2));        // 2
查看输出与解释
3
4
0
-1
-1
2

实现采用左闭右开区间,当 nums[mid] <= target 时左界推进,否则右界收紧;最终 left 为首个大于 target 的下标,返回前校验 left-1 处是否等于 target。特殊输入下仍保持 O(log n) 时间,O(1) 空间。

日志恢复中的游标续传

假设事故恢复系统里有一个按时间戳严格升序排列的消息数组,每条记录带毫秒级时间,业务需要找到“某指定时间戳最后一次写入”的下标,以便从它后面继续拉取未消费消息。输入给定数组 [100,200,200,200,300] 且目标 200,函数应返回 3 而不是 1。调用上面的二分实现,命中索引之后直接得到续传游标,避免一条条线性检查。

这种场景强调的不是能不能找到一个值,而是重复时间戳出现多批时,取最大值才不丢数据。若用普通二分返回任意一个等于目标的下标,游标可能停在中间导致部分日志被重复处理;用上界回退恰好给出连续重复段的末尾,语义与“最后写入”完全对应。因为在分布式系统中日志常有大纪元重复,复杂度仍是 O(log n),空间 O(1)。

易错边界与防御检查

最容易失效的输入是 target 并不存在。例如数组 [1,2,4] 中找 3,循环结束后 left = 2(因为第一个比3大的是4的下标2),left-1 = 1,nums[1] 是2不等于3,所以必须检查“left-1 合法 && nums[left-1] === target”才返回索引,否则返回-1。另一个极端是 target 大于数组全部元素,例如找9时 left=n,此时 left-1 索引 n-1 仍不等于9,直接返回-1即可;若是数组只有1个重复元素也一样。

空数组与单元素数组也容易让新手慌乱。空数组时 left=0,条件守卫会短路并返回-1;单元素且匹配时,如 [7] 找7,left=1,left-1=0匹配返回0。若要扩展成找第一个大于等于(lower bound)、第一个大于(upper bound)等变体,只需反转 <= 为 < 就能切换语义,这种对称性是二分模板的核心价值。代价是把“是否找到”的判断留到函数最后,比直接返回−1的位置多一次比较,但仍保持 O(log n)。

回答前,多想一步

容易答错的地方

遇到等于目标值就返回当前 mid
mid 可能不是重复段末尾,尤其在目标在左侧或右侧还有更多相同元素时,返回 mid 会提前终止。应该继续压缩区间,将相等情况视为仍需向右探测。
认为只需反向改动不等号即可
找最后一个与找第一个并非简单把 < 改成 > 就能完成。换位符号的同时还要决定哪边继续推进、返回哪个边界。应保持 <= 推进左界、> 收紧右界,最后取 left−1 并做相等校验。
试着用自己的话回答

面试官还会怎么问?

能否直接用 while (left <= right) 的闭区间实现?

可以。取 mid 时相等条件下 left = mid + 1,循环结束后 left 指向首个更大元素,返回 left-1 并验证是否为目标即可。闭区间写法需要小心 right 初始化为 n-1,终止状态更容易差一。

如果数组里全是重复目标,这个查找的最坏情况复杂度怎样?

仍是 O(log n)。因为每次更新不变量都会让 mid 位置确定,不变量与重复元素数量无关。即使整个数组都为 target,寻找上界的区间长度在每一步都减半,只需要约 log2(n) 次比较。

如果要找最后一个小于目标的位置,怎么改?

把判据 nums[mid] < target 放入左侧推进分支,循环结束后 left 是第一个不小于 target 的位置,那么 left-1 恰好是最后一个小于 target 的位置。这与上界模板一脉相承。

从一道题,走向一组知识

把知识连起来

搜索与排序

二分查找如何找到目标值的第一个出现位置?

本题目的是最后一个出现,与同专题的首次出现实现对称,对照阅读能分清左闭右开区间与等号方向的细节。

搜索与排序

二分查找失败时如何返回目标值应插入的位置?

插入位置问题本质是求上界或下界,本解法返回的 left-1 与插入语义紧密相连,可帮助统一理解二分边界。

参考资料

  • Binary search¶

示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。

本题目录
  1. 先记住这个答案
  2. 以上界为桥的实现逻辑
  3. 日志恢复中的游标续传
  4. 易错边界与防御检查
  5. 容易答错的地方
  6. 面试官还会怎么问
  7. 把知识连起来
读懂,再试着讲出来

先看核心答案,再读代码。最后展开追问,检查自己有没有遗漏边界。

试着回答追问
浏览全部面试题理解原理,也关注真实的使用场景。回到顶部 ↑