278. 第一个错误的版本
问题描述
你是产品经理,目前正在带领一个团队开发新的产品。不幸的是,你的产品的最新版本没有通过质量检测。由于每个版本都是基于之前的版本开发的,所以错误的版本之后的所有版本都是错的。
假设你有 n 个版本 [1, 2, ..., n],你想找出第一个错误的版本,从而减少回归测试的工作量。
给你一个 API bool isBadVersion(version),它会返回版本 version 是否是错误的。请你设计一个算法来查找第一个错误的版本。你应该尽量减少对 API 的调用次数。
示例:
输入: n = 5, bad = 4
输出: 4
解释:
调用 isBadVersion(3) -> false
调用 isBadVersion(5) -> true
调用 isBadVersion(4) -> true
所以,4 是第一个错误的版本。
输入: n = 1, bad = 1
输出: 1
算法思路
二分查找法:
- 利用"错误版本之后的所有版本都是错的"这一特性
- 使用二分查找在有序序列中寻找第一个满足条件的位置
- 当
isBadVersion(mid)为true时,第一个错误版本在左半部分(包含mid) - 当
isBadVersion(mid)为false时,第一个错误版本在右半部分(不包含mid)
核心:这是一个典型的"寻找第一个满足条件的元素"问题,适合用二分查找解决。
代码实现
方法一:标准二分查找(推荐解法)
/* The isBadVersion API is defined in the parent class VersionControl.
boolean isBadVersion(int version); */
public class Solution extends VersionControl {
/**
* 查找第一个错误的版本
*
* @param n 版本总数
* @return 第一个错误版本的编号
*/
public int firstBadVersion(int n) {
// 初始化搜索区间:[left, right]
// left 表示可能的第一个错误版本的最小值
// right 表示可能的第一个错误版本的最大值
int left = 1;
int right = n;
// 二分查找主循环
// 当 left < right 时继续搜索
while (left < right) {
// 计算中点,避免整数溢出
// 使用 left + (right - left) / 2 而不是 (left + right) / 2
int mid = left + (right - left) / 2;
// 调用API检查中间版本是否为错误版本
if (isBadVersion(mid)) {
// mid 是错误版本
// 说明第一个错误版本在 [left, mid] 范围内
// 因为 mid 可能就是第一个错误版本,所以 right = mid
right = mid;
} else {
// mid 不是错误版本
// 说明第一个错误版本在 (mid, right] 范围内
// 因为 mid 肯定不是错误版本,所以 left = mid + 1
left = mid + 1;
}
}
// 循环结束时 left == right
// 此时 left 就是第一个错误版本的位置
return left;
}
}
方法二:递归二分查找
public class Solution extends VersionControl {
/**
* 递归实现二分查找找第一个错误版本
*
* @param n 版本总数
* @return 第一个错误版本的编号
*/
public int firstBadVersion(int n) {
return findFirstBadVersion(1, n);
}
/**
* 递归辅助函数:在 [left, right] 区间内查找第一个错误版本
*
* @param left 搜索区间的左边界(包含)
* @param right 搜索区间的右边界(包含)
* @return 第一个错误版本的编号
*/
private int findFirstBadVersion(int left, int right) {
// 递归终止条件:区间只有一个元素
if (left == right) {
return left;
}
// 计算中点
int mid = left + (right - left) / 2;
if (isBadVersion(mid)) {
// mid 是错误版本,第一个错误版本在左半部分
return findFirstBadVersion(left, mid);
} else {
// mid 不是错误版本,第一个错误版本在右半部分
return findFirstBadVersion(mid + 1, right);
}
}
}
方法三:优化(处理边界情况)
public class Solution extends VersionControl {
/**
* 优化版本:特别处理边界情况
*
* @param n 版本总数
* @return 第一个错误版本的编号
*/
public int firstBadVersion(int n) {
// 特殊情况处理
if (n == 1) {
return 1;
}
int left = 1;
int right = n;
// 使用位运算优化除法操作
// >> 1 相当于 / 2,但效率更高
while (left < right) {
int mid = left + ((right - left) >> 1);
if (isBadVersion(mid)) {
right = mid;
} else {
left = mid + 1;
}
}
return left;
}
}
算法分析
-
时间复杂度:O(log n)
- 每次都将搜索空间减半,最多需要 log₂n 次 API 调用
- 例如 n=1000 时,最多只需要 10 次调用
-
空间复杂度:
- 迭代版本:O(1),只使用常数额外空间
- 递归版本:O(log n),递归调用栈的深度
-
API调用次数:最多 ⌊log₂n⌋ + 1 次
算法过程
n = 5, 第一个错误版本是 4 :
- 初始:
left=1, right=5 - 第1轮:
mid = 1 + (5-1)/2 = 3isBadVersion(3)→false(不是错误版本)left = 4, right = 5
- 第2轮:
mid = 4 + (5-4)/2 = 4isBadVersion(4)→true(是错误版本)right = 4, left = 4
- 循环结束:
left == right == 4 - 返回 4
总共调用 isBadVersion API 2 次。
测试用例
// 注意:以下测试代码需要在实际环境中运行
// 因为 isBadVersion() 方法由系统提供
public static void main(String[] args) {
Solution solution = new Solution();
// 测试用例1:标准情况
int result1 = solution.firstBadVersion(5);
// 假设第4个版本是第一个错误版本
System.out.println("Test 1: " + result1); // 期望输出: 2
// 测试用例2:最小情况
int result2 = solution.firstBadVersion(1);
System.out.println("Test 2: " + result2); // 期望输出: 1
// 测试用例3:第一个版本就是错误版本
int result3 = solution.firstBadVersion(10);
// 假设第1个版本就是错误版本
System.out.println("Test 3: " + result3); // 期望输出:
// 测试用例4:最后一个版本才是错误版本
int result4 = solution.firstBadVersion(10);
// 假设第10个版本是第一个错误版本
System.out.println("Test 4: " + result4); // 期望输出:
// 测试用例5:较大范围
int result5 = solution.firstBadVersion(2126753390);
// 假设某个中间版本是第一个错误版本
System.out.println("Test 5: " + result5);
}
关键点
-
搜索区间的定义:
- 使用左闭右闭区间
[left, right] - 循环条件为
while (left < right)
- 使用左闭右闭区间
-
中点计算:
- 必须使用
left + (right - left) / 2避免整数溢出 - 当
left和right都接近 Integer.MAX_VALUE 时,left + right会溢出
- 必须使用
-
边界更新策略:
isBadVersion(mid) == true:right = mid(包含mid)isBadVersion(mid) == false:left = mid + 1(不包含mid)
-
循环终止条件:
- 使用
left < right而不是left <= right - 避免
无限循环和不必要的API调用
- 使用
-
返回值:
- 循环结束时
left == right,返回left即可
- 循环结束时
常见问题
-
为什么 right = mid 而不是 mid - 1?
- 因为
mid可能就是第一个错误版本,不能排除 - 如果
mid是错误版本,第一个错误版本可能就是mid或在左边
- 因为
-
为什么 left = mid + 1?
- 因为
mid已经被确认不是错误版本,可以安全排除
- 因为
-
如何处理 n=0 的情况?
- 题目保证 n ≥ 1,无需特殊处理
-
能否用线性搜索?
- 可以,但时间复杂度 O(n),在 n 很大时效率极低
- 二分查找 O(log n) 是最优解
-
API调用次数有上限吗?
- LeetCode 通常有调用次数限制
- 二分查找能保证最少的调用次数
扩展思考
-
如果错误版本不是连续的?
- 本题的前提条件不成立,需要重新设计算法
-
如果有多个产品线并行开发?
- 可能需要分布式二分查找或并行搜索策略
-
如何最小化最坏情况下的API调用次数?
- 二分查找已经是最优策略,平均和最坏情况都是 O(log n)
-
如果 isBadVersion() 调用很耗时?
- 二分查找能最大程度减少调用次数
- 可以考虑缓存结果避免重复调用
282

被折叠的 条评论
为什么被折叠?



