Skip to content

Commit e04b46a

Browse files
committed
201
1 parent 618c312 commit e04b46a

File tree

3 files changed

+353
-1
lines changed

3 files changed

+353
-1
lines changed

SUMMARY.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -178,4 +178,6 @@
178178
* [191. Number of 1 Bits](leetcode-191-Number-of-1-Bits.md)
179179
* [198. House Robber](leetcode-198-House-Robber.md)
180180
* [199. Binary Tree Right Side View](leetcode-199-Binary-Tree-Right-Side-View.md)
181-
* [200. Number of Islands](leetcode-200-Number-of-Islands.md)
181+
* [200. Number of Islands](leetcode-200-Number-of-Islands.md)
182+
* [201 题到 300 题](leetcode-201-300.md)
183+
* [201. Bitwise AND of Numbers Range](leetcode-201-Bitwise-AND-of-Numbers-Range.md)

leetcode-201-300.md

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# leetcode 201 到 300 题
2+
3+
<a href="leetcode-201-Bitwise-AND-of-Numbers-Range.html">201. Bitwise AND of Numbers Range</a>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,347 @@
1+
# 题目描述(中等难度)
2+
3+
![](https://windliang.oss-cn-beijing.aliyuncs.com/201.jpg)
4+
5+
给一个闭区间的范围,将这个范围内的所有数字相与,返回结果。例如 `[5, 7]` 就返回 `5 & 6 & 7`
6+
7+
# 解法一 暴力
8+
9+
写一个 `for` 循环,依次相与即可。
10+
11+
```java
12+
public int rangeBitwiseAnd(int m, int n) {
13+
int res = m;
14+
for (int i = m + 1; i <= n; i++) {
15+
res &= i;
16+
}
17+
return res;
18+
}
19+
```
20+
21+
然后会发现时间超时了。
22+
23+
![](https://windliang.oss-cn-beijing.aliyuncs.com/201_2.jpg)
24+
25+
当范围太大的话会造成超时,这里优化的话想法也很很简单。我们只需要在 `res == 0` 的时候提前出 `for` 循环即可。
26+
27+
```java
28+
public int rangeBitwiseAnd(int m, int n) {
29+
int res = m;
30+
for (int i = m + 1; i <= n; i++) {
31+
res &= i;
32+
if(res == 0){
33+
return 0;
34+
}
35+
}
36+
return res;
37+
}
38+
```
39+
40+
但接下来遇到了 `wrong answer`
41+
42+
![](https://windliang.oss-cn-beijing.aliyuncs.com/201_3.jpg)
43+
44+
把这个样例再根据代码理一遍,就会发现大问题了,根本原因就是补码的原因,可以看一下 [趣谈计算机补码](https://zhuanlan.zhihu.com/p/67227136)
45+
46+
右边界 `n``2147483647`,也就是 `Integer` 中最大的正数,二进制形式是 `01111...1`,其中有 `31``1`。在代码中当 `i` 等于 `n` 的时候依旧会进入循环。出循环执行 `i++`,我们期望它变成 `2147483647 + 1 = 2147483648`,然后跳出 `for` 循环。事实上,对 `2147483647``1`,也就是 `01111...1``1`,变成了 `1000..000`,其中有 `31``1`。而这个二进制在补码中表示的是 `-2147483648`。因此我们依旧会进入 `for` 循环,以此往复,直到结果是 `0` 才出了 `for` 循环。。
47+
48+
知道了这个,我们只需要判断 `i == 2147483647` 的话,就跳出 `for` 循环即可。
49+
50+
```java
51+
public int rangeBitwiseAnd(int m, int n) {
52+
//m 要赋值给 i,所以提前判断一下
53+
if(m == Integer.MAX_VALUE){
54+
return m;
55+
}
56+
int res = m;
57+
for (int i = m + 1; i <= n; i++) {
58+
res &= i;
59+
if(res == 0 || i == Integer.MAX_VALUE){
60+
break;
61+
}
62+
}
63+
return res;
64+
}
65+
```
66+
67+
上边的解法就是我能想到的了,然后就去逛 `Discuss` 了,简直大开眼界。下边分享一下,主要是两种思路。
68+
69+
# 解法二
70+
71+
参考 [这里](https://leetcode.com/problems/bitwise-and-of-numbers-range/discuss/56729/Bit-operation-solution(JAVA))
72+
73+
我们只需要一个经常用的一个思想,去考虑子问题。我们现在要做的是把从 `m``n` 的所有数字的 `32` 个比特位依次相与。直接肯定不能出结果,如果要是只考虑 `31` 个比特位呢,还是不能出结果。然后依次降低规模,`30``29` ... `3``2` 直到 `1`。如果让你说出从 `m``n` 的数字全部相与,最低位的结果是多少呢?
74+
75+
最低位会有很多数相与,要么是 `0` ,要么是 `1`,而出现了 `0` 的话相与的结果一定会是 `0`
76+
77+
只看所有数的最低位变化情况,`m``n` 的话,要么从 `0` 开始递增,`01010101...`,要么从 `1` 开始递增 `10101010...`
78+
79+
因此,参与相与的数中最低位要么在第一个数字第一次出现 `0` ,要么在第二个数字出现第一次出现 `0`
80+
81+
如果 `m < n`,也就是参与相与的数字的个数至少有两个,所以一定会有 `0` 的出现,所以相与结果一定是 `0`
82+
83+
看具体的例子,`[5,7]`
84+
85+
```java
86+
最低位序列从 1 开始递增, 也就是最右边的一列 101
87+
m 5 1 0 1
88+
6 1 1 0
89+
n 7 1 1 1
90+
0
91+
```
92+
93+
此时 `m < n`,所以至少会有两个数字,所以最低位相与结果一定是 `0`
94+
95+
解决了最低位的问题,我们只需要把 `m``n` 同时右移一位。然后继续按照上边的思路考虑新的最低位的结果即可。
96+
97+
而当 `m == n` 的时候,很明显,结果就是 `m` 了。
98+
99+
代码中,我们需要用一个变量 `zero` 记录我们右移的次数,也就是最低位 `0` 的个数。
100+
101+
```java
102+
public int rangeBitwiseAnd(int m, int n) {
103+
int zeros = 0;
104+
while (n > m) {
105+
zeros++;
106+
m >>>= 1;
107+
n >>>= 1;
108+
}
109+
//将 0 的个数空出来
110+
return m << zeros;
111+
}
112+
```
113+
114+
然后还有一个优化的手段,在 [191 题](https://leetcode.wang/leetcode-191-Number-of-1-Bits.html) 介绍过一个把二进制最右边 `1` 置为 `0` 的方法,在这道题中也可以用到。
115+
116+
> 有一个方法,可以把最右边的 `1` 置为 `0`,举个具体的例子。
117+
>
118+
> 比如十进制的 `10`,二进制形式是 `1010`,然后我们只需要把它和 `9` 进行按位与操作,也就是 `10 & 9 = (1010) & (1001) = 1000`,也就是把 `1010` 最右边的 `1` 置为 `0`
119+
>
120+
> 规律就是对于任意一个数 `n`,然后 `n & (n-1)` 的结果就是把 `n` 的最右边的 `1` 置为 `0`
121+
>
122+
> 也比较好理解,当我们对一个数减 `1` 的话,比如原来的数是 `...1010000`,然后减一就会向前借位,直到遇到最右边的第一个 `1`,变成 `...1001111`,然后我们把它和原数按位与,就会把从原数最右边 `1` 开始的位置全部置零了 `...10000000`
123+
124+
这里的话我们考虑一种可以优化的情况,我们直接用 `n` 这个变量去保存最终的结果,只需要考虑 `n` 的低位的 `1` 是否需要置为 `0`
125+
126+
```java
127+
m X X X X X X X X
128+
...
129+
n X X X X 1 0 0 0
130+
131+
此时 m < n,上边的解法中然后我们会依次进行右移,我们考虑把 n 低位的 0 移光直到 1 移动到最低位
132+
133+
m2 X X X X X
134+
...
135+
n2 X X X X 1
136+
137+
此时如果 m2 < n2,那么我们就可以确定最低位相与的结果一定是 0
138+
139+
回到开头 , n 的低位都是 0, 所以从 m < n 一定可以推出 m2 < n2, 所以最终结果的当前位一定是 0
140+
141+
因此,如果 m < n ,我们只需要把 n ,也就是 X X X X 1 0 0 0 的最右边的 10, 然后继续考虑。
142+
```
143+
144+
代码的话,用前边介绍的 `n & (n - 1)`
145+
146+
```java
147+
public int rangeBitwiseAnd(int m, int n) {
148+
int zeros = 0;
149+
while (n > m) {
150+
n = n & (n - 1);
151+
}
152+
return n;
153+
}
154+
```
155+
156+
# 解法三
157+
158+
参考了 [这篇](https://leetcode.com/problems/bitwise-and-of-numbers-range/discuss/56827/Fast-three-line-C%2B%2B-solution-and-explanation-with-no-loops-or-recursion-and-one-extra-variable)[这篇](https://leetcode.com/problems/bitwise-and-of-numbers-range/discuss/56735/Java-8-ms-one-liner-O(log(32))-no-loop-no-explicit-log)
159+
160+
解法的关键就是去考虑这样一个问题,一个数大于一个数意味着什么?或者说,怎么判断一个数大于一个数?
161+
162+
在十进制中,我们只需要从高位向低位看去,直到某一位不相同,大小也就判断了出来。
163+
164+
比如 `6489...``6486...`,由于 `9 > 6`,所以不管后边的位是什么 `6489...` 一定会大于 ``6486...``
165+
166+
那么对于二进制呢?
167+
168+
一样的道理,但因为二进制只有两个数 `0``1`,所以当出现某一位大于另一位的时候,一定是 `1 > 0`
169+
170+
所以对于 `[m n]`,如果 `m < n`,那么一定是下边的形式。
171+
172+
```java
173+
m S S S 0 X X X X
174+
n S S S 1 X X X X
175+
```
176+
177+
前边的若干位都相同,然后从某一位开始从 `0` 变成 `1`
178+
179+
所有数字相与的结果,结合解法一的结论,如果 `n > m`,最低位相与后是 `0`。最后一定是 `S S S 0 0 0 0 0` 的形式。
180+
181+
因为高位保证了 `m``n` 同时右移以后,依旧是 `n > m`
182+
183+
```java
184+
m S S S 0 X X X X
185+
n S S S 1 X X X X
186+
187+
此时 n > m, 所以最低位结果是 0
188+
189+
然后 m 和 n 同时右移
190+
191+
m S S S 0 X X X
192+
n S S S 1 X X X
193+
依旧是 n > m, 所以最低位结果是 0
194+
```
195+
196+
因此相与结果最低位一直是 `0`,一直到 `S S S` 。所以最终结果就是 `S S S 0 0 0 0 0`
197+
198+
其实和解法一的第二种思想有些类似,解法一中我们是从右往左依次将 `1` 置为 `0`。而在这里,我们从左往右看,找到第一个 `0``1`,就保证了移位过程中一定是 `n > m`
199+
200+
知道了这个结论,我们只需要把 `m``11..1X0..00` 相与即可。上边例子中,我们只需要把 `S S S 0 X X X``1 1 1 X 0 0 0` 相与即可。
201+
202+
那么怎么得到 `1 1 1 X 0 0 0` 呢?
203+
204+
再观察一下,`m``n`
205+
206+
```java
207+
m S S S 0 X X X X
208+
n S S S 1 X X X X
209+
```
210+
211+
我们如果把 `m``n` 进行异或操作,结果就是 `0 0 0 1 X X X X`
212+
213+
对比一下异或后的结果和最后我们需要的结果。
214+
215+
```java
216+
当前结果 0 0 0 1 X X X X
217+
最后结果 1 1 1 X 0 0 0 0
218+
```
219+
220+
首先我们需要将低位全部变成 `0`
221+
222+
```java
223+
当前结果 0 0 0 1 0 0 0 0
224+
最后结果 1 1 1 X 0 0 0 0
225+
```
226+
227+
`java` 中有个方法可以实现,`Integer.highestOneBit`,可以实现保留最高位的 `1` ,然后将其它位全部置为 `0`。即,把 `0 0 0 1 X X X X` 变成 `0 0 0 1 0 0 0 0`
228+
229+
继续看上边的对比,接下来我们要把高位的 `0` 变为 `1`,通过取反操作,变成下边的结果。
230+
231+
```java
232+
当前结果 1 1 1 0 1 1 1 1
233+
最后结果 1 1 1 X 0 0 0 0
234+
```
235+
236+
然后再在当前结果加 `1`,就实现了我们的转换。
237+
238+
```java
239+
当前结果 1 1 1 1 0 0 0 0
240+
最后结果 1 1 1 X 0 0 0 0
241+
```
242+
243+
把最终得到的结果和 `m` 相与即可,`m == n` 的情况单独考虑。
244+
245+
```java
246+
public int rangeBitwiseAnd(int m, int n) {
247+
if (m == n) {
248+
return m;
249+
}
250+
return m & ~Integer.highestOneBit(m ^ n) + 1;
251+
}
252+
```
253+
254+
结合 [补码](https://zhuanlan.zhihu.com/p/67227136) 的知识,「按位取反,末尾加 1」其实相当于取了一个相反数,[29 题](https://leetcode.wang/leetCode-29-Divide-Two-Integers.htmlhttps://leetcode.wang/leetCode-29-Divide-Two-Integers.html) 中我们也运用过这个结论。所以代码可以写的更简洁一些。
255+
256+
```java
257+
public int rangeBitwiseAnd(int m, int n) {
258+
return m == n ? m : m & -Integer.highestOneBit(m ^ n);
259+
}
260+
```
261+
262+
我们调用了库函数 `Integer.highestOneBit`,我们去看一下它的实现。
263+
264+
```java
265+
/**
266+
* Returns an {@code int} value with at most a single one-bit, in the
267+
* position of the highest-order ("leftmost") one-bit in the specified
268+
* {@code int} value. Returns zero if the specified value has no
269+
* one-bits in its two's complement binary representation, that is, if it
270+
* is equal to zero.
271+
*
272+
* @param i the value whose highest one bit is to be computed
273+
* @return an {@code int} value with a single one-bit, in the position
274+
* of the highest-order one-bit in the specified value, or zero if
275+
* the specified value is itself equal to zero.
276+
* @since 1.5
277+
*/
278+
public static int highestOneBit(int i) {
279+
// HD, Figure 3-1
280+
i |= (i >> 1);
281+
i |= (i >> 2);
282+
i |= (i >> 4);
283+
i |= (i >> 8);
284+
i |= (i >> 16);
285+
return i - (i >>> 1);
286+
}
287+
```
288+
289+
它做了什么事情呢?
290+
291+
对于 `0 0 0 1 X X X X` ,最终会变成 `0 0 0 1 1 1 1 1`,记做 `i` 。把 `i` 再右移一位变成 `0 0 0 0 1 1 1 1`,然后两数做差。
292+
293+
```java
294+
i 0 0 0 1 1 1 1 1
295+
i >>> 1 0 0 0 0 1 1 1 1
296+
0 0 0 1 0 0 0 0
297+
```
298+
299+
就得到了这个函数最后返回的结果了。
300+
301+
`0 0 0 1 X X X X` 变成 `0 0 0 1 1 1 1 1`,可以通过复制实现。
302+
303+
第一步,将首位的 `1` 赋值给它的旁边。
304+
305+
```java
306+
i |= (i >> 1);
307+
0 0 0 1 X X X X -> 0 0 0 1 1 X X X
308+
309+
现在首位有两个 1 了,所以就将这两个 1 看做一个整体,继续把 1 赋值给它的旁边。
310+
i |= (i >> 2);
311+
0 0 0 1 1 X X X -> 0 0 0 1 1 1 1 X
312+
313+
现在首位有 41 了,所以就将这 41 看做一个整体,继续把 1 赋值给它的旁边。
314+
i |= (i >> 4);
315+
0 0 0 1 1 1 1 X -> 0 0 0 1 1 1 1 1
316+
317+
其实到这里已经结束了,但函数中是考虑最坏的情况,类似于这种 1000000...00, 首位是 1, 有 310
318+
```
319+
320+
通过移位变成了 `0 0 0 1 1 1 1 1`,回想一下我们之前分析的,我们需要 `1 1 1 X 0 0 0` 的结果,和当前移位后的结果对比,我们只需要取反就可以得到了,最后和 `m` 相与即可。
321+
322+
```java
323+
public int rangeBitwiseAnd(int m, int n) {
324+
if (m == n) {
325+
return m;
326+
}
327+
int i = m ^ n;
328+
i |= (i >>> 1);
329+
i |= (i >>> 2);
330+
i |= (i >>> 4);
331+
i |= (i >>> 8);
332+
i |= (i >>> 16);
333+
return m & ~i;
334+
}
335+
```
336+
337+
#
338+
339+
解法一只要注意溢出的问题即可。
340+
341+
解法二考虑的时候是从右往左考虑,解法三是从左往右考虑,但是殊途同归,本质上,两种解法都是求了两个数字的最长相同前缀,然后低位补零。
342+
343+
解法二中,我们不停的右移或者将右边的 `1` 置为 `0`,就是把不是相同前缀的部分置为 `0`,直到二者相等,也就是只剩下了相同前缀。
344+
345+
解法三中,通过异或,直接把相同前缀部分置为了 `0`。然后通过某种方法把相同前缀对应部分置为 `1` 来提取相同前缀。
346+
347+
这个题,太神奇了,太妙了!

0 commit comments

Comments
 (0)