@TOC

接上文LeetCode算法题解——二分查找1,本篇总结LeetCode中部分题目的二分查找解法。

第四题(寻找左边界)

278. 第一个错误的版本

题目描述:
你是产品经理,目前正在带领一个团队开发新的产品。不幸的是,你的产品的最新版本没有通过质量检测。由于每个版本都是基于之前的版本开发的,所以错误的版本之后的所有版本都是错的。

假设你有 n 个版本 [1, 2, …, n],你想找出导致之后所有版本出错的第一个错误的版本。

你可以通过调用 bool isBadVersion(version) 接口来判断版本号 version 是否在单元测试中出错。实现一个函数来查找第一个错误的版本。你应该尽量减少对调用 API 的次数。

示例1:

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

思路

如果将n个版本的true和false放到一个数组里,应该是[false, false, …, true, true],前面都是false,后面都是true。这道题可以看作是找到最后一个false,也可以看作是找到第一个true。在这里,我将它当作找到第一个true来做,也就是寻找true的左边界。

代码

java版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Solution extends VersionControl {
public int firstBadVersion(int n) {
// 左闭右闭
int l = 1;
int r = n;
// 寻找true的左边界
while(l <= r){
// 防止溢出
int m = l + (r - l) / 2;
// 能找到,缩小右边界
if(isBadVersion(m) == true){
r = m - 1;
}
// 找不到,缩小左边界
else if(isBadVersion(m) == false){
l = m + 1;
}
}
return l;
}
}

python版本

1
2
3
4
5
6
7
8
9
10
class Solution:
def firstBadVersion(self, n: int) -> int:
l, r = 1, n
while l <= r:
m = l + (r - l) // 2
if isBadVersion(m):
r = m - 1
else:
l = m + 1
return l

总结

在寻找左边界的时候,和之前中点值找到target一样,找到就收缩右边界。拓展一下,在寻找左/右边界,找到了target,但还没有到达其边界,应该向你要搜索的边界靠近,进一步压缩搜索空间。

第五题(寻找右边界)

744. 寻找比目标字母大的最小字母

题目描述:
给你一个字符数组 letters,该数组按非递减顺序排序,以及一个字符 target。letters 里至少有两个不同的字符。

返回 letters 中大于 target 的最小的字符。如果不存在这样的字符,则返回 letters 的第一个字符。

示例1:

1
2
3
输入: letters = ["c", "f", "j"],target = "a"
输出: "c"
解释:letters 中字典上比 'a' 大的最小字符是 'c'。

思路

按照题意,寻找第一个比target大的字母。在前面寻找右边界时我们已经提到,当循环停止时右边界r以右的元素都大于target,则右边界r以左的元素都小于等于target,右边界r即最后一个target。所以,第一个比target大的字母就是r+1处的字母。但是当target比数组中所有数字都要大的时候,r+1会越界,这里需要做特别处理。

代码

java版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Solution {
public char nextGreatestLetter(char[] letters, char target) {
// 左闭右闭
int l = 0;
int r = letters.length - 1;
// 寻找右边界
while(l <= r){
// 防止溢出
int m = l + (r - l) / 2 ;
// 收缩左边界
if(target >= letters[m]){
l = m + 1;
}
// 收缩右边界
else{
r = m - 1;
}
}
// 防止越界,当不存在这样的字符,即target最大
if(r == letters.length - 1){
return letters[0];
}
return letters[r + 1];
}
}

python版本

1
2
3
4
5
6
7
8
9
10
class Solution:
def nextGreatestLetter(self, letters: List[str], target: str) -> str:
l, r = 0, len(letters) - 1
while l <= r:
m = l + (r - l) // 2
if target >= letters[m]:
l = m + 1
else:
r = m - 1
return letters[(r + 1) % len(letters)]

总结

这道题,需要将大于 target 的最小的字符理解成第一个比target大的字符,并且知道可以通过寻找target的右边界来求解。

第六题(找一个数)

367. 有效的完全平方数

题目描述:
给你一个正整数 num 。如果 num 是一个完全平方数,则返回 true ,否则返回 false 。

完全平方数 是一个可以写成某个整数的平方的整数。换句话说,它可以写成某个整数和自身的乘积。

不能使用任何内置的库函数,如 sqrt 。

示例1:

1
2
3
输入:num = 16
输出:true
解释:返回 true ,因为 4 * 4 = 16 且 4 是一个整数。

思路

首先思考一种暴力算法。以num=16为例,我们先从1开始穷举。1*1=1,1<16,不满足。2*2=4,4<16,不满足。3*3=9,9<16,不满足。4*4=16,16==16,满足!5*5=25,25>16,不满足。显然,比4小的数和比4大的数,都不满足,我们可以通过二分法筛选掉比4小的数和比4大的数。

值得注意的是,如果直接比较x*x和num,可能x*x会超过int的范围。可以转化为比较x和num/x。如果是这样的话,考虑到整数除法结果仍然为整数,当x=num/x时,num可能是x*x,x*x+1,…,x*x+(x-1)。此时只需要再次比较x*x和num,即可。

代码

java版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Solution {
public boolean isPerfectSquare(int num) {
// 左闭右闭
int l = 1;
int r = num;
while(l <= r){
// 防止溢出
int m = l + (r - l) / 2;
int sqrt = num / m;
// 找到target并进行判断
if((m == sqrt) && (m * sqrt == num)){
return true;
}
else if(m < sqrt){
l = m + 1;
}
else{
r = m - 1;
}
}
return false;
}
}

python版本

1
2
3
4
5
6
7
8
9
10
11
12
class Solution:
def isPerfectSquare(self, num: int) -> bool:
l, r = 1, num
while l <= r:
m = l + (r - l) // 2
sqrt = num // m
if sqrt < m:
r = m - 1
else:
l = m + 1

return r * r == num

总结

这道题,我们可以先思考一个穷举的办法,然后考虑可以用二分法进行优化。防止溢出是一个需要注意的点。这道题里,m是中点,sqrt是target。和以前不同,这道题的target是会变的。

第七题(找右边界)

69. x 的平方根

题目描述:
给你一个非负整数 x ,计算并返回 x 的 算术平方根 。

由于返回类型是整数,结果只保留 整数部分 ,小数部分将被 舍去 。

注意:不允许使用任何内置指数函数和算符,例如 pow(x, 0.5) 或者 x ** 0.5 。

示例1:

1
2
3
输入:x = 8
输出:2
解释:8 的算术平方根是 2.82842..., 由于返回类型是整数,小数部分将被舍去。

思路

这题和上一题不同。在上一题中,完全平方数,如16,一定能找到整数4*4=16。但是在这题里,将平方根的小数部分舍去了,那么就不能通过x*x=num来进行判断。同样的,我们先思考一种暴力算法。以num=8为例,我们先从1开始穷举。1*1=1,1<8;2*2=4,4<8;3*3=9,9>8。显然,前面的平方数都比8小,后面的平方数都比8大,我们需要找到最后一个比8小的平方数。问题转化为了找比8小的平方数的右边界。

和上一题一样,如果直接比较x*x和num,可能x*x会超过int的范围,可以转化为比较x和num/x。

代码

java版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution {
public int mySqrt(int x) {
if(x <= 1){
return x;
}
// 左闭右闭
int l = 1;
int r = x;
while(l <= r){
// 防止溢出
int m = l + (r - l) / 2;
int sqrt = x / m;
if(m > sqrt){
r = m - 1;
}
else{
l = m + 1;
}
}
return r;
}
}

python版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution:
def mySqrt(self, x: int) -> int:
if x == 0:
return x

l, r = 1, x
while l <= r:
m = l + (r - l) // 2
sqrt = x // m
if sqrt < m:
r = m - 1
else:
l = m + 1
return r

总结

同样的,这道题我们先思考一个穷举的办法,然后考虑可以用二分法进行优化。防止溢出依旧是一个需要注意的点。这道题里,m是中点,sqrt是target,会发生改变。

下一篇博客LeetCode算法题解——二分查找3中,我将分享LeetCode中几道比较难想到使用二分查找解法的题目。

参考:

二分查找算法细节详解,顺便写了首诗