力扣 No.278 二分法的简单应用

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

文章目录

  • 前言
  • 一、暴力法(会报错——超出时间限制)
  • 二、自己用折半查找的优化(超出内存限制)
  • 三、正确题解
  • 总结


前言

你是产品经理,目前正在带领一个团队开发新的产品。不幸的是,你的产品的最新版本没有通过质量检测。由于每个版本都是基于之前的版本开发的,所以错误的版本之后的所有版本都是错的。
假设你有 n 个版本 [1, 2, …, n],你想找出导致之后所有版本出错的第一个错误的版本。
你可以通过调用 bool isBadVersion(version) 接口来判断版本号 version 是否在单元测试中出错。实现一个函数来查找第一个错误的版本。你应该尽量减少对调用 API 的次数。


最大的要求是“尽量减少对API的调用次数”

一、暴力法(会报错——超出时间限制)

一开始的想法非常简单,直接对1——n进行遍历,当返回true 的时候就是第一个错误的版本号。但是这样做的时间复杂度就是O(n),不能通过提交。

/* The isBadVersion API is defined in the parent class VersionControl.
      boolean isBadVersion(int version); */

public class Solution extends VersionControl {
    public int firstBadVersion(int n) {
        for (int i = 1; i <= n; i++) {
            if (isBadVersion(i)){
                return i;
            }
        }
        return 0;
    }
}

二、自己用折半查找的优化(超出内存限制)

先贴出自己的的代码(自己写了测试用例和API接口)

public class BadVersion {
    public static void main(String[] args) {
        int n = firstBadVersion(5);
        System.out.println(n);
    }
    public static int firstBadVersion(int n) {
        //将版本序列构造成数组。
        int[] nums = new int[n];
        for (int i = 1; i <= n; i++) {
            nums[i-1] = i;
        }

        //使用二分搜索找到第一个ture对应的序列号
        int low = 0;
        int high = nums.length - 1;
        while (low <= high){
            int mid = (low + high)/2;
            //如果mid的值是true,代表这是个错误序号,第一个错误在他之前
            if (isBadVersion(nums[mid])){
                //如果mid的前一个是false,就代表这是第一个
                if (!isBadVersion(nums[mid-1])){
                    return mid + 1; //由于mid是角标,而数组从1开始,所以结果需要+1
                }else { //如果mid的前一个是true,就代表这不是第一个错误在前,需要移动high指针
                    high = mid - 1;
                }
            }else {     //如果mid的值是false,代表这是个正确的序号,第一个错误在他之后,需要移动low指针
                low = mid + 1;
            }
        }
        return -1;
    }
    public static boolean isBadVersion(int version){
        if (version >= 4){
            return true;
        }else{
            return false;
        }
    }
}

这样做可以通过一些测试用例,但是当mid=0的时候会报错,找到了两个问题:

  1. 没有考虑mid=0的情况,这会让mid-1的时候抛出异常
  2. 当n的值过大时,其low和high的折半计算会溢出

经过优化后——

while (low <= high){
            int mid = (low + high)/2;
            //如果mid的值是true,代表这是个错误序号,第一个错误在他之前
            if (isBadVersion(nums[mid])){
                //如果mid的前一个是false,就代表这是第一个
                if (mid != 0 && !isBadVersion(nums[mid-1])){
                    return mid + 1; //由于mid是角标,而数组从1开始,所以结果需要+1
                }else if(mid == 0){
                    return 1;
                }else { //如果mid的前一个是true,就代表这不是第一个错误在前,需要移动high指针
                    high = mid - 1;
                }
            }else {     //如果mid的值是false,代表这是个正确的序号,第一个错误在他之后,需要移动low指针
                low = mid + 1;
            }
        }

三、正确题解

仍然报错超出内存限制,于是查看了官方的题解如下——

/* The isBadVersion API is defined in the parent class VersionControl.
      boolean isBadVersion(int version); */

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; // 答案在区间 [left, mid] 中
                } else {
                    left = mid + 1; // 答案在区间 [mid+1, right] 中
                }
            }
            // 此时有 left == right,区间缩为一个点,即为答案
            return left;
        }
    }

结论是我想的复杂了一些,对true返回值的前后做了过多的分类,导致if语句过多,占用了内存空间。答案的方法更加简洁明了,如果返回true则第一个true在前半部分,应该移动high。如果返回false则第一个true还在后半部分,应该移动low。其实就是非常正常的二分法。


总结

  1. 使用mid = left + (right - left) / 2; 来防止计算时溢出
  2. 考虑解法时尽量减少不必要的分类讨论,可以节省空间。

你可能感兴趣的:(力扣刷题笔记,leetcode,算法,职场和发展)