|
| 1 | +# 题目描述(简单难度) |
| 2 | + |
| 3 | + |
| 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