提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
前言
你是产品经理,目前正在带领一个团队开发新的产品。不幸的是,你的产品的最新版本没有通过质量检测。由于每个版本都是基于之前的版本开发的,所以错误的版本之后的所有版本都是错的。
假设你有 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的时候会报错,找到了两个问题:
- 没有考虑mid=0的情况,这会让mid-1的时候抛出异常
- 当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。其实就是非常正常的二分法。
总结
- 使用mid = left + (right - left) / 2; 来防止计算时溢出
- 考虑解法时尽量减少不必要的分类讨论,可以节省空间。