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

一次学会二分法——力扣278.第一个错误的版本

力扣278.第一个错误的版本

在这里插入图片描述


LeetCode 278 — 第一个错误的版本:多方法详解与比较(含代码与复杂度分析)

题目要求:给定版本号 1…n,已知从某个版本 bad 开始之后所有版本都是错误的,提供接口 isBadVersion(version),找出第一个错误的版本,尽量减少对 isBadVersion 的调用次数。


一、问题回顾与本质分析

每个版本要么是好的(good),要么是坏的(bad),并且版本是基于前一版本开发的,因此存在单调性:

good, good, ..., good, bad, bad, ..., bad

我们要寻找第一个 bad 出现的位置。这是典型的「在单调序列中查找满足条件的第一个位置」问题,最自然的解法是二分查找,可以将对 isBadVersion 的调用次数降到对数级别 O(log n)


二、方法一:标准二分查找(迭代,闭区间)

思路要点

  • 使用左右闭区间 [left, right] 保存可能的解;
  • 每次选 mid = left + (right - left) / 2
  • isBadVersion(mid)true,答案在 [left, mid](包含 mid);
  • 否则答案在 [mid+1, right]
  • 循环终止条件 left == right,返回 left

代码(Java)

public class Solution extends VersionControl {public int firstBadVersion(int n) {int left = 1, right = n;while (left < right) {int mid = left + (right - left) / 2;if (isBadVersion(mid)) {right = mid;      // mid 可能是第一个坏版本} else {left = mid + 1;   // 第一个坏版本在右侧}}return left;}
}

在这里插入图片描述

复杂度

  • 时间:O(log n)isBadVersion 调用
  • 空间:O(1)

三、方法二:递归二分(闭区间)

思路

与方法一逻辑相同,不过用递归实现。递归深度为 O(log n)

代码(Java)

public class Solution extends VersionControl {public int firstBadVersion(int n) {return helper(1, n);}private int helper(int left, int right) {if (left == right) return left;int mid = left + (right - left) / 2;if (isBadVersion(mid)) {return helper(left, mid);} else {return helper(mid + 1, right);}}
}

在这里插入图片描述

复杂度

  • 时间:O(log n)
  • 空间:递归栈 O(log n)

注意:递归实现简洁但依赖语言的递归栈限制(本题 n 最大为 2^31 - 1,但实际递归深度为 log2 n,一般安全)。


四、方法三:半开区间二分([left, right))

思路

使用半开区间 [left, right),更利于避免 off-by-one 错误:

  • 初始 left = 1, right = n + 1
  • 循环 while (left < right)mid = left + (right - left) / 2
  • isBadVersion(mid)true,则 right = mid;否则 left = mid + 1
  • 返回 left

代码(Java)

public class Solution extends VersionControl {public int firstBadVersion(int n) {long left = 1, right = (long)n + 1; // 避免 n+1 溢出while (left < right) {long mid = left + (right - left) / 2;if (isBadVersion((int)mid)) {right = mid;} else {left = mid + 1;}}return (int)left;}
}

在这里插入图片描述

复杂度

  • 时间:O(log n)
  • 空间:O(1)

五、方法四:指数(跳跃)查找 + 二分(当第一个坏版本很靠前时更省 API 调用)

适用场景

firstBad(第一个坏版本)非常靠近开头(k << n)时,先做指数增长定位区间,然后在区间内做二分,可将调用次数降到 O(log k),比直接对整个区间做 O(log n) 更少。

步骤

  1. 检查 isBadVersion(1),若为 true 返回 1;
  2. 指数增长 bound = 1, 2, 4, 8, ...,直到 isBadVersion(bound)truebound > n
  3. 在区间 (bound/2, min(bound, n)] 上做二分查找。

代码(Java)

public class Solution extends VersionControl {public int firstBadVersion(int n) {if (isBadVersion(1)) return 1;long bound = 1;while (bound <= n && !isBadVersion((int)bound)) {bound <<= 1;}int left = (int)(bound / 2) + 1;int right = (int)Math.min(bound, n);while (left < right) {int mid = left + (right - left) / 2;if (isBadVersion(mid)) right = mid;else left = mid + 1;}return left;}
}

在这里插入图片描述

复杂度

  • 调用次数:O(log k),其中 k 为第一个坏版本的位置(若 k 接近 n,仍为 O(log n)
  • 空间:O(1)

六、方法五:并行 / 分布式

在实际工程中,如果 isBadVersion 接口耗时较长且允许并发,可以并行检测多个版本以减少总体延迟。但这通常会增加 API 调用总量,不符合题目“最少调用次数”的目标。LeetCode 环境也不支持并行调用,所以此方法只做工程层面的说明。


七、常见陷阱与注意事项

  1. 整型溢出

    • 避免使用 (left + right) / 2,应使用 left + (right - left) / 2left + ((right - left) >> 1) 来计算中点,防止 left + right 超出 int 范围。
  2. 区间边界 off-by-one

    • 选择闭区间还是半开区间要一致。半开区间经常能减少边界错误。
  3. 接口副作用

    • 假设 isBadVersion 是幂等的、无副作用的。如果接口有副作用或昂贵的计算,应尽量减少重复调用。
  4. 递归栈深度

    • 递归实现使用 O(log n) 的栈深度,通常安全,但对非常受限的环境还是谨慎使用。

八、方法对比与推荐

方法调用次数(数量级)额外空间是否推荐
标准闭区间二分O(log n)O(1)推荐(常用)
递归二分O(log n)O(log n) 栈可以(取决于偏好)
半开区间二分O(log n)O(1)推荐(防 off-by-one)
指数跳跃 + 二分O(log k)(k=firstBad)O(1)特殊场景下推荐
并行分块视实现需要并发支持工程场景考虑,不用于 LeetCode

推荐做法:一般直接使用“标准迭代二分”或“半开区间二分”。如果你知道或怀疑第一个坏版本很靠前,使用“指数跳跃 + 二分”可以减少实际调用次数。


九、测试建议(本地调试)

在本地调试时,可以模拟 isBadVersion,并统计调用次数以比较不同方法的实际性能。示例框架如下:

public class VersionControl {private int bad;public int cnt = 0;public VersionControl(int bad) { this.bad = bad; }public boolean isBadVersion(int v) {cnt++;return v >= bad;}
}

isBadVersion 的调用代理给 VersionControl,然后运行不同实现并输出 cnt 统计,观察调用次数差异。


十、总结

  • 本题本质是单调序列上找第一个满足条件的位置,二分查找是最自然、最优的解法(O(log n) 次 API 调用)。
  • 推荐使用迭代形式实现二分(闭区间或半开区间),既高效又易于遵循边界。
  • 在特殊场景下(第一个坏版本很靠前),指数跳跃 + 二分可以进一步减少调用次数。
  • 实工程中可结合并发策略优化响应时间,但会增加调用次数并不适用于竞赛或 LeetCode 题目约束。
http://www.dtcms.com/a/512333.html

相关文章:

  • 数据结构——二十七、十字链表与邻接多重链表(王道408)
  • 网站公司做的网站被攻击苏州网络推广
  • 网站权重能带来什么作用灰大设计导航网
  • i.MX6ULL Linux内核启动流程深度解析
  • Browser-Use 打造可操作浏览器的 AI 智能体
  • php网站开发入门到精通教程好玩的游戏网页
  • 代码仓库码云(gitee)配置环境记录
  • 织梦网站模板陶瓷广州建设行业网站
  • 面试(六)——Java IO 流
  • 怎么做视频网站教程php彩票网站建设教程
  • 大模型(Large Language Model, LLM)——什么是大模型,大模型的基本原理、架构、流程
  • 长春网站建设排名怎样用自己电脑做网站
  • 基于 Redis 的基数统计:高效的大规模去重与计数
  • 机械外贸网站站长网站工具
  • 广州企业建站素材安徽禹尧工程建设有限公司网站
  • MySQL if函数
  • Promise.all怎么用
  • 成都网站建设开发价玉环哪里有做网站
  • 01)mysql数据误删恢复相关-mysql5.7 开启 binlog、设置binlog 保留时间
  • 电力电子技术 第五章——非连续导电模式
  • Django 项目 .gitignore 模板
  • MySQL 中文排序(拼音排序)不生效问题全解析
  • 建站网络公司云南网站备案难吗
  • 深度学习(8)- PyTorch 数据处理与加载
  • JAVA:Spring Boot 集成 Jackson 实现高效 JSON 处理
  • 深度学习之YOLO系列YOLOv4
  • 江西移动网站建站推广外包
  • 张家口网站建设zjktao温州公司网址公司
  • Cef笔记:Cef消息循环的集成
  • 第十六篇:Lambda表达式:匿名函数对象的艺术