第一个错误的版本 图解题解
这道题到底在问什么
- 输入
- n = 5,第 4 版起坏:好好好坏坏
- 输出
- 4(第一个坏版本的下标)
最优解:为什么这么做
一句话答案:LeetCode 278 第一个错误的版本:版本是「好…好坏…坏」的单调序列,二分找第一个坏的位置。mid 坏就 r=mid 保住候选、好就 l=mid+1,每次砍半把调用从 O(n) 压到 O(log n)、空间 O(1)。
第一个错误的版本这道题在找什么
有 n 个版本,编号 1 到 n,从某个版本起坏,且一旦坏就之后全坏。你只有一个接口 isBadVersion(v),问它第 v 版坏没坏、坏就返回 true。要找出第一个坏版本的编号,且调用次数尽量少。题面例子:n=5、第 4 版起坏,答案是 4。
为什么从头一个个问会太慢
从版本 1 挨个问 isBadVersion(1)、isBadVersion(2)……问到第一个返回 true 就停,能找到,但最坏要问将近 n 次;n 是十亿就得问接近十亿次,等不起。根子是没用上「一旦坏则之后全坏」这个结构——版本排得很有规律,逐个扫却当它杂乱一堆。
为什么这道题能二分
「一旦坏则之后全坏」意味着版本前一段全好、后一段全坏,中间只翻一次脸:「好好好坏坏坏」。isBadVersion 的返回值也从 false 变成 true、只翻这一次,是一条 false…false true…true 的分界。找第一个坏版本,就是找这条分界上第一个 true 的位置(这类『找第一个满足条件的下标』叫 lower_bound)。有这种单调(一个方向到底、不回头)结构,就能二分查找,不必逐格看。
mid 坏往哪边收、mid 好往哪边收
候选区间 [l, r],l 左边界、r 右边界,第一个坏版本一定夹在中间。每轮取中点 mid(区间正中那个编号,取 l+(r-l)//2),只问一次 isBadVersion(mid)。它坏:答案是 mid 或在它左边,右边界收到 r = mid——是 mid 不是 mid-1,mid 自己可能就是答案、减一会丢掉它。它好:答案在 mid 右边,mid 连同左边全排除,l 推到 mid+1。答案始终在 [l, r] 内,循环到 l 和 r 撞在一起就是答案。
n=5、第 4 版起坏,逐问缩区间
版本坏好是「好好好坏坏」,l=1、r=5。第一轮 mid = 1+(5-1)//2 = 3,问 isBadVersion(3),版本 3 好,于是 l = 3+1 = 4。第二轮区间剩 [4, 5],mid = 4+(5-4)//2 = 4,问 isBadVersion(4),版本 4 坏,于是 r = 4。此时 l、r 都等于 4,循环停下,返回 4。整趟只问了 2 次 isBadVersion。
右界多减一,就吞掉第一个坏版本
mid 坏说明答案在 mid 或更左,写成 r = mid-1 就等于断定 mid 不是答案、把它踢出区间,真答案恰是 mid 时就返回成它右边那个,错。好版本那支 l 必须是 mid+1,漏了加一、让 l 停在 mid,好版本还赖在区间里,区间永远缩不动,直接死循环。还有 mid 一律用 l+(r-l)//2,别写 (l+r)//2,l、r 接近整型上限时相加会溢出。这三处对了,每问一次砍掉一半候选,次数就是 O(log n),n=10 亿也不过约 30 次;只用 l、r、mid 三个变量,空间 O(1)。
▶ 动画逐步走查(共 21 步)——想跟着动画一帧帧对照就展开
- 3两条规则记牢:mid 坏 → r = mid(答案可能就是它,不能丢);mid 好 → l = mid+1(它和它左边全排除)。区间每轮砍半,撞到一起就是答案。
- 4第一遍:第 9 版起坏。一开始谁坏谁好都不知道,候选区间是全部 13 个版本,左边界 l 在版本 1、右边界 r 在版本 13。
- 5取区间正中间:mid 落在版本 7。调用 isBadVersion(7),问问它坏没坏——这是这一轮唯一的一次提问。
- 6版本 7 是好的(标绿)。第一个坏版本一定在它右边,连它带左边一起排除,左边界推到下一格:l = mid + 1。
- 7取区间正中间:mid 落在版本 10。调用 isBadVersion(10),问问它坏没坏——这是这一轮唯一的一次提问。
- 8版本 10 是坏的(标红)。第一个坏版本要么就是它、要么在它更左边,所以右边界收到这里:r = mid。它右边那些更不用看了。
- 9取区间正中间:mid 落在版本 9。调用 isBadVersion(9),问问它坏没坏——这是这一轮唯一的一次提问。
- 10版本 9 是坏的(标红)。第一个坏版本要么就是它、要么在它更左边,所以右边界收到这里:r = mid。它右边那些更不用看了。
- 11取区间正中间:mid 落在版本 8。调用 isBadVersion(8),问问它坏没坏——这是这一轮唯一的一次提问。
- 12版本 8 是好的(标绿)。第一个坏版本一定在它右边,连它带左边一起排除,左边界推到下一格:l = mid + 1。
- 13l 和 r 撞到同一格,区间只剩一个候选——它就是第一个坏版本:版本 9。整轮只问了几次就锁定,远少于一个个试。
- 14第二遍:第 4 版起坏(换个答案再走一遍)。一开始谁坏谁好都不知道,候选区间是全部 13 个版本,左边界 l 在版本 1、右边界 r 在版本 13。
- 15取区间正中间:mid 落在版本 7。调用 isBadVersion(7),问问它坏没坏——这是这一轮唯一的一次提问。
- 16版本 7 是坏的(标红)。第一个坏版本要么就是它、要么在它更左边,所以右边界收到这里:r = mid。它右边那些更不用看了。
- 17取区间正中间:mid 落在版本 4。调用 isBadVersion(4),问问它坏没坏——这是这一轮唯一的一次提问。
- 18版本 4 是坏的(标红)。第一个坏版本要么就是它、要么在它更左边,所以右边界收到这里:r = mid。它右边那些更不用看了。
- 19取区间正中间:mid 落在版本 2。调用 isBadVersion(2),问问它坏没坏——这是这一轮唯一的一次提问。
- 20版本 2 是好的(标绿)。第一个坏版本一定在它右边,连它带左边一起排除,左边界推到下一格:l = mid + 1。
- 21取区间正中间:mid 落在版本 3。调用 isBadVersion(3),问问它坏没坏——这是这一轮唯一的一次提问。
- 22版本 3 是好的(标绿)。第一个坏版本一定在它右边,连它带左边一起排除,左边界推到下一格:l = mid + 1。
- 23l 和 r 撞到同一格,区间只剩一个候选——它就是第一个坏版本:版本 4。整轮只问了几次就锁定,远少于一个个试。
⚠️ 容易写错的地方
✗ 错:mid 坏时写成 r = mid - 1
✓ 对:mid 坏时 r = mid
mid 自己可能就是第一个坏版本,减一会把正确答案丢出区间
✗ 错:mid 好时写成 l = mid
✓ 对:mid 好时 l = mid + 1
mid 已确认是好的,不加一会导致区间不缩小、死循环
✗ 错:用 mid = (l + r) / 2
✓ 对:用 mid = l + (r - l) / 2
l+r 在 n 很大时会整数溢出;后者等价但永不溢出
完整代码(Python / C++ / Java)
Python
def firstBadVersion(n):
l, r = 1, n # 候选区间 [l, r]
while l < r:
mid = l + (r - l) // 2 # 防溢出的取中点
if isBadVersion(mid): # mid 坏:答案在它或它左边
r = mid # 不能丢掉 mid,所以是 mid 不是 mid-1
else: # mid 好:答案在它右边
l = mid + 1
return l # l == r 时即第一个坏版本C++
int firstBadVersion(int n){
int l = 1, r = n;
while (l < r) {
int mid = l + (r - l) / 2;
if (isBadVersion(mid)) r = mid;
else l = mid + 1;
}
return l;
}Java
public int firstBadVersion(int n) {
int l = 1, r = n;
while (l < r) {
int mid = l + (r - l) / 2;
if (isBadVersion(mid)) r = mid;
else l = mid + 1;
}
return l;
}复杂度
时间
O(log n)
每轮把候选区间砍一半,最多问 log₂n 次就收敛
空间
O(1)
只用 l、r、mid 三个变量,不开额外空间
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 第一个错误的版本 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么循环条件是 l < r,而不是 l <= r?+
因为收缩规则里 r = mid 不减一,区间右端可能就是答案、必须留在里面。配 l < r,当 l 和 r 撞成一格时循环正好停下,那一格既没被排除、又是唯一候选,直接返回它即可。若改成 l <= r 又保持 r = mid,某些情况下 l、r 会卡在同一格反复取同一个 mid,进入死循环。想用 l <= r,就得换另一套写法(r = mid-1 再加一个记录答案的变量),两套别混着写。
r = mid 和 l = mid+1,为什么一个不减一、一个要加一?+
关键看 mid 本身还算不算候选。mid 坏时,它有可能正是第一个坏版本,不能排除,所以 r 收到 mid、把它留在区间。mid 好时,它绝不可能是第一个坏版本,已被彻底排除,l 就要跨过它到 mid+1。如果这里不加一、让 l = mid,好版本 mid 还赖在区间里,区间缩不动,就死循环了。一个保、一个弃,方向正好相反。
为什么调用次数是 O(log n),n=10 亿具体是多少次?+
每问一次 isBadVersion,候选区间就砍掉一半,从 n 缩到 n/2、n/4……一直到只剩一格。缩多少次到 1,就是问多少次,等于 log₂n。n=10 亿时 log₂(10^9) 约等于 30,所以最多约 30 次调用就能锁定第一个坏版本,比逐个问的近十亿次少了几千万倍。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 第一个错误的版本 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。