Skip to content

Commit 6a7a508

Browse files
committed
278
1 parent 3d7329e commit 6a7a508

File tree

2 files changed

+153
-0
lines changed

2 files changed

+153
-0
lines changed

SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,4 +231,5 @@
231231
* [273. Integer to English Words](leetcode-273-Intege-to-English-Words.md)
232232
* [274. H-Index](leetcode-274-H-Index.md)
233233
* [275. H-Index II](leetcode-275-H-IndexII.md)
234+
* [278. First Bad Version](leetcode-278-First-Bad-Version.md)
234235
* [更多](more.md)

leetcode-278-First-Bad-Version.md

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
# 题目描述(简单难度)
2+
3+
![](https://windliang.oss-cn-beijing.aliyuncs.com/278.jpg)
4+
5+
题目说的比较绕,其实就是在 `false false false true true` 这样的序列中找出第一次出现 `true` 的位置。
6+
7+
可以通过 `isBadVersion` 函数得到当前位置是 `false` 还是 `true`
8+
9+
# 解法一
10+
11+
最直接的解法,从 `1` 开始遍历,依次判断是否是 `true`
12+
13+
```java
14+
public int firstBadVersion(int n) {
15+
for (int i = 1; i < n; i++) {
16+
if (isBadVersion(i)) {
17+
return i;
18+
}
19+
}
20+
return -1;
21+
}
22+
```
23+
24+
没想到这个解法竟然会超时。
25+
26+
# 解法二
27+
28+
`false false false true true` 可以想成有序数组,`0 0 0 1 1`,寻找第一次出现 `1` 的位置。
29+
30+
自然会想到二分查找了,和 [275 题](https://leetcode.wang/leetcode-275-H-IndexII.html) 解法是一样的,当时是找到第一个出现在直线上方的点。
31+
32+
```java
33+
public int firstBadVersion(int n) {
34+
int low = 1;
35+
int high = n;
36+
while (low <= high) {
37+
int mid = (low + high) >>> 1;
38+
if (isBadVersion(mid)) {
39+
if (mid == 1) {
40+
return 1;
41+
}
42+
//判断前一个是否是 false
43+
if (!isBadVersion(mid - 1)) {
44+
return mid;
45+
}
46+
high = mid - 1;
47+
} else {
48+
low = mid + 1;
49+
}
50+
}
51+
return -1;
52+
}
53+
```
54+
55+
[275 题](https://leetcode.wang/leetcode-275-H-IndexII.html) 一样,我觉得上边的解法比较好理解也就没写其他的写法了,没想到又碰到这种题了,那顺便再说一下其他的写法吧。
56+
57+
上边是采取提前结束的方法,事实上,因为数组中一定会有一个 `true` ,所以我们确信一定会找到我们要寻找的值。
58+
59+
所以我们可以通过不断的缩小范围,直到数组中只剩下一个位置,那么这个位置就一定是我们要找的。
60+
61+
下边的解法保证每次循环我们要找到解都在 `low``high` 之间,从而当 `low == high` 的时候,此时剩下的最后一个数就是我们要找的了。
62+
63+
```java
64+
public int firstBadVersion(int n) {
65+
int low = 1;
66+
int high = n;
67+
//这里去除等于,只剩一个值的时候就跳出来
68+
while (low < high) {
69+
int mid = (low + high) >>> 1;
70+
if (isBadVersion(mid)) {
71+
//这里不再是 mid - 1, 因为 mid 有可能是我们要找的值
72+
high = mid;
73+
} else {
74+
low = mid + 1;
75+
}
76+
}
77+
return low;
78+
}
79+
```
80+
81+
还有一种写法,比较反直觉。
82+
83+
```java
84+
public int firstBadVersion(int n) {
85+
int low = 1;
86+
int high = n;=
87+
while (low <= high) {
88+
int mid = (low + high) >>> 1;
89+
if (isBadVersion(mid)) {
90+
high = mid - 1;
91+
} else {
92+
low = mid + 1;
93+
}
94+
}
95+
return low;
96+
}
97+
```
98+
99+
`high = mid - 1` ,看起来会把我们要找的值丢掉。其实是没有关系的,如果 `mid` 值是我们要找的,那么后续 `low` 会不断向 `high` 靠近,当 `low``high` 相等的时候,`low` 最后更新 `low = mid + 1` ,刚好又回到了我们要找的值。
100+
101+
还有一种情况,如果之前一直没有找到我们要找的值,直到最后一步 `low == high` 的时候才找到。此时会进入 `if` 语句,更新 `high = mid - 1` 错过我们要找的值。但没有关系,我们返回的是 `low` ,依旧是我们要找的值。
102+
103+
# 扩展 求中点
104+
105+
[108 题](https://leetcode.wang/leetcode-108-Convert-Sorted-Array-to-Binary-Search-Tree.html) 已经说过这个扩展了,由于经常用到,这里再贴过来,如果不清楚的话可以看一下。
106+
107+
前几天和同学发现个有趣的事情,分享一下。
108+
109+
首先假设我们的变量都是 `int` 值。
110+
111+
二分查找中我们需要根据 `start``end` 求中点,正常情况下加起来除以 2 即可。
112+
113+
```java
114+
int mid = (start + end) / 2
115+
```
116+
117+
但这样有一个缺点,我们知道`int`的最大值是 `Integer.MAX_VALUE` ,也就是`2147483647`。那么有一个问题,如果 `start = 2147483645``end = 2147483645`,虽然 `start``end`都没有超出最大值,但是如果利用上边的公式,加起来的话就会造成溢出,从而导致`mid`计算错误。
118+
119+
解决的一个方案就是利用数学上的技巧,我们可以加一个 `start` 再减一个 `start` 将公式变形。
120+
121+
```java
122+
(start + end) / 2 = (start + end + start - start) / 2 = start + (end - start) / 2
123+
```
124+
125+
这样的话,就解决了上边的问题。
126+
127+
然后当时和同学看到`jdk`源码中,求`mid`的方法如下
128+
129+
```java
130+
int mid = (start + end) >>> 1
131+
```
132+
133+
它通过移位实现了除以 2,但。。。这样难道不会导致溢出吗?
134+
135+
首先大家可以补一下 [补码](https://mp.weixin.qq.com/s/uvcQHJi6AXhPDJL-6JWUkw) 的知识。
136+
137+
其实问题的关键就是这里了`>>>` ,我们知道还有一种右移是`>>`。区别在于`>>`为有符号右移,右移以后最高位保持原来的最高位。而 `>>>` 这个右移的话最高位补 0。
138+
139+
所以这里其实利用到了整数的补码形式,最高位其实是符号位,所以当 `start + end` 溢出的时候,其实本质上只是符号位收到了进位,而`>>>`这个右移不仅可以把符号位右移,同时最高位只是补零,不会对数字的大小造成影响。
140+
141+
`>>` 有符号右移就会出现问题了,事实上 JDK6 之前都用的`>>`,这个 BUG 在 java 里竟然隐藏了十年之久。
142+
143+
#
144+
145+
还是典型的二分查找的应用。解法一就不说了,说一下解法二的三种写法。
146+
147+
我觉得第一种写法比较直观,每次判断一下我们是否找到了,可以提前结束。
148+
149+
第二种写法的话也经常用到,比如 [33 题](https://leetcode.wang/leetCode-33-Search-in-Rotated-Sorted-Array.html) 找最小值下标的时候,当时和 [@为爱卖小菜](https://leetcode-cn.com/u/wei-ai-mai-xiao-cai/) 讨论了很多,自己对二分理解也深刻了不少。这种写法用于一定可以找到解的时候,一定要注意的是 `low < high`,不能加等号,不然可能造成死循环。
150+
151+
第三种写法的话,这里就不推荐了。
152+

0 commit comments

Comments
 (0)