【刷题之路】LeetCode 278. 第一个错误的版本

一、题目描述

原题连接: 278. 第一个错误的版本

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

示例 1:

输入: n = 5, bad = 4
输出: 4
解释:
调用 isBadVersion(3) -> false
调用 isBadVersion(5) -> true
调用 isBadVersion(4) -> true
所以,4 是第一个错误的版本。

示例 2:

输入: n = 1, bad = 1
输出: 1

二、解题

1、方法1——直接遍历

1.1、思路解析

直接遍历所有版本,根据题目描述,第一次遇到错误的版本就可以返回。
在这里插入图片描述

1.2、代码实现

有了以上思路,那我们写起代码来也就水到渠成了:

int firstBadVersion1(int n) {
	int i = 0;
	for (i = 1; i <= n; i++) {
		if (true == isBadVersion(i)) {
			return i;
		}
	}
	return -1;
}

时间复杂度:O(n),n为版本数量,最坏情况下我们需要遍历完所有的版本。
空间复杂度:O(1),我们只需要用到常数级的额外空间。

2、方法2——二分法

1.1、二分法——极限区间

1.1.1、思路分析

由题目描述我们可知正确的版本的左侧所有版本都为正确版本:
在这里插入图片描述
而错误版本的右侧的所有版本都为错误的版本:
在这里插入图片描述

我们可以利用这一点,将我们的区间长度一直缩小到1,具体做法如下:
当isBadVersion(mid) == false时,说明错误的版本只有可能出现在mid右侧,所以转而判断区间[mid + 1, right],执行left = mid + 1:
在这里插入图片描述
当isBadVersion(mid) == true时,说明第一个错误的版本可能出现在mid的左侧,也有可能此时的mid就是第一个错误的版本,
所以转而判断区间[left, mid],执行right = mid:
在这里插入图片描述
最后当left == right时,我们就找到了第一个错误的版本,直接返回left或right都行。
在这里插入图片描述

1.1.2、代码实现

有了以上思路,那我们写起代码来也就水到渠成了:

int firstBadVersion2(int n) {
	int left = 0;
	int right = n;
	int mid = 0;
	while (left < right) {
		mid = left + (right - left) / 2;
		if (isBadVersion(mid)) {
			right = mid;
		}
		else {
			left = mid + 1;
		}
	}
	return left;
}

时间复杂度:O(log2N),N为总的版本数。
空间复杂度:O(1),我们只需要用到常数级的额外空间。

1.2、二分法——直接定位

1.2.1、思路分析

由题目描述我们可知:
当 isBadVersion(mid - 1) == false && isBadVersion(mid) == false时,说明mid的左端的所有版本都不是错误的,所以查找区间应该转换成[mid + 1, rihgt],执行left = mid + 1:
在这里插入图片描述
当 isBadVersion(mid - 1) == true && isBadVersion(mid) == true时,说明mid现在所在的区间正位于错误的版本序列中,为了找到第一个错误的版本,区间应该向左边缩小,缩小成[left, mid]:
在这里插入图片描述

只有当 isBadVersion(mid - 1) == false && isBadVersion(mid) == true时,则说明此时的mid就是第一个错误的版本,直接返回mid即可:
在这里插入图片描述

1.2.2、代码实现

有了以上思路,那我们写起代码来也就水到渠成了:

int firstBadVersion3(int n) {
	int left = 0;
	int right = n;
	int mid = 0;
	while (left <= right) {
		mid = left + (right - left) / 2;
		if (isBadVersion(mid - 1) == false && isBadVersion(mid) == false) {
			left = mid + 1;
		}
		else if (isBadVersion(mid - 1) == true && isBadVersion(mid) == true) {
			right = mid;
		}
		else if (isBadVersion(mid - 1) == false && isBadVersion(mid) == true) {
			return mid;
		}
	}
	return -1;
}

时间复杂度:O(log2N),N为总的版本数。
空间复杂度:O(1),我们只需要用到常数级的额外空间。
这个方法使用mid直接定位目标,就不用每次都把区间缩短到1,从而在时间开销上比上一个算法好一些。

1.3、二分法——判断边界

1.3.1、思路分析

注意到left移动的条件为:isBadVersion(mid - 1) == false && isBadVersion(mid) == false,且移动的方式是left = mid + 1:
在这里插入图片描述
所以我们其实可以直接就判断left是否为错误版本,当第一次出现 isBadVersion(left) == true时,就说明left就是第一个出错的版本,
直接返回left即可。
所以我们的操作就变成了:
当isBadVersion(mid - 1) == false && isBadVersion(mid) == false时,执行left = mid + 1;
其他情况一律执行right = mid,使区间向左端缩小。

1.3.2、代码实现

有了以上思路,那我们写起代码来也就水到渠成了:

int firstBadVersion4(int n) {
	int left = 0;
	int right = n;
	int mid = 0;
	while (left <= right) {
		if (isBadVersion(left) == true) {
			return left;
		}
		mid = left + (right - left) / 2;
		if (isBadVersion(mid - 1) == false && isBadVersion(mid) == false) {
			left = mid + 1;
		}
		else {
			right = mid;
		}
	}
	return -1;
}

时间复杂度:O(log2N),N为总的版本数。
空间复杂度:O(1),我们只需要用到常数级的额外空间。
这个方法对上一个方法作出的改进是:避免了在left已经指向错误版本后还要不断向左端逼近的繁琐操作。

递归版本——空间换时间
其实这个算法我觉得写成递归的会比较好一点:

// 先写一个递归版本的找边界函数
int binary_search(int left, int right) {
	if (isBadVersion(left) == true) {
		return left;
	}
	int mid = left + (right - left) / 2;
	if (isBadVersion(mid - 1) == false && isBadVersion(mid) == false) {
		return binary_search(mid + 1, right);
	}
	else {
		return binary_search(left, mid);
	}
}

int firstBadVersion5(int n) {
	return binary_search(1, n);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

林先生-1

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值