算法题 第一个错误的版本

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

算法思路

二分查找法

  1. 利用"错误版本之后的所有版本都是错的"这一特性
  2. 使用二分查找在有序序列中寻找第一个满足条件的位置
  3. isBadVersion(mid)true 时,第一个错误版本在左半部分(包含mid)
  4. 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

  1. 初始left=1, right=5
  2. 第1轮
    • mid = 1 + (5-1)/2 = 3
    • isBadVersion(3)false(不是错误版本)
    • left = 4, right = 5
  3. 第2轮
    • mid = 4 + (5-4)/2 = 4
    • isBadVersion(4)true(是错误版本)
    • right = 4, left = 4
  4. 循环结束left == right == 4
  5. 返回 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);
}

关键点

  1. 搜索区间的定义

    • 使用左闭右闭区间 [left, right]
    • 循环条件为 while (left < right)
  2. 中点计算

    • 必须使用 left + (right - left) / 2 避免整数溢出
    • leftright 都接近 Integer.MAX_VALUE 时,left + right 会溢出
  3. 边界更新策略

    • isBadVersion(mid) == trueright = mid(包含mid)
    • isBadVersion(mid) == falseleft = mid + 1(不包含mid)
  4. 循环终止条件

    • 使用 left < right 而不是 left <= right
    • 避免无限循环和不必要的API调用
  5. 返回值

    • 循环结束时 left == right,返回 left 即可

常见问题

  1. 为什么 right = mid 而不是 mid - 1?

    • 因为 mid 可能就是第一个错误版本,不能排除
    • 如果 mid 是错误版本,第一个错误版本可能就是 mid 或在左边
  2. 为什么 left = mid + 1?

    • 因为 mid 已经被确认不是错误版本,可以安全排除
  3. 如何处理 n=0 的情况?

    • 题目保证 n ≥ 1,无需特殊处理
  4. 能否用线性搜索?

    • 可以,但时间复杂度 O(n),在 n 很大时效率极低
    • 二分查找 O(log n) 是最优解
  5. API调用次数有上限吗?

    • LeetCode 通常有调用次数限制
    • 二分查找能保证最少的调用次数

扩展思考

  1. 如果错误版本不是连续的?

    • 本题的前提条件不成立,需要重新设计算法
  2. 如果有多个产品线并行开发?

    • 可能需要分布式二分查找或并行搜索策略
  3. 如何最小化最坏情况下的API调用次数?

    • 二分查找已经是最优策略,平均和最坏情况都是 O(log n)
  4. 如果 isBadVersion() 调用很耗时?

    • 二分查找能最大程度减少调用次数
    • 可以考虑缓存结果避免重复调用
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值