Skip to content

Commit ef51e7e

Browse files
committed
0
1 parent cef7a4b commit ef51e7e

File tree

2 files changed

+218
-0
lines changed

2 files changed

+218
-0
lines changed

SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,4 +224,5 @@
224224
* [242. Valid Anagram](leetcode-242-Valid-Anagram.md)
225225
* [257. Binary Tree Paths](leetcode-257-Binary-Tree-Paths.md)
226226
* [258. Add Digits](leetcode-258-Add-Digits.md)
227+
* [260. Single Number III](leetcode-260-Single-NumberIII.md)
227228
* [更多](more.md)

leetcode-260-Single-NumberIII.md

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
# 题目描述(中等难度)
2+
3+
![](https://windliang.oss-cn-beijing.aliyuncs.com/260.jpg)
4+
5+
所有数字都出现了两次,只有两个数字都只出现了 `1` 次,找出这两个数字。
6+
7+
# 解法一
8+
9+
最直接的方法,统计每个数出现的次数。使用 `HashMap` 或者 `HashSet`,由于每个数字最多出现两次,我们可以使用 `HashSet`
10+
11+
遍历数组,遇到的数如果 `HashSet` 中存在,就把这个数删除。如果不存在,就把它加入到 `HashSet` 中。最后 `HashSet` 中剩下的两个数就是我们要找的了。
12+
13+
```java
14+
public int[] singleNumber(int[] nums) {
15+
HashSet<Integer> set = new HashSet<>();
16+
for (int n : nums) {
17+
if (set.contains(n)) {
18+
set.remove(n);
19+
} else {
20+
set.add(n);
21+
}
22+
}
23+
int[] result = new int[2];
24+
int i = 0;
25+
for (int n : set) {
26+
result[i] = n;
27+
i++;
28+
}
29+
return result;
30+
}
31+
```
32+
33+
# 解法二
34+
35+
我们之前做过 [136 题](https://leetcode.wang/leetcode-136-Single-Number.html) ,当时是所有数字都是成对出现的,只有一个数字是落单的,找出这个落单的数字。其中介绍了异或的方法,把之前的介绍先粘贴过来。
36+
37+
还记得位操作中的异或吗?计算规则如下。
38+
39+
> 0 ⊕ 0 = 0
40+
>
41+
> 1 ⊕ 1 = 0
42+
>
43+
> 0 ⊕ 1 = 1
44+
>
45+
> 1 ⊕ 0 = 1
46+
47+
总结起来就是相同为零,不同为一。
48+
49+
根据上边的规则,可以推导出一些性质
50+
51+
- 0 ⊕ a = a
52+
- a ⊕ a = 0
53+
54+
此外异或满足交换律以及结合律。
55+
56+
所以对于之前的例子 `a b a b c c d` ,如果我们把给定的数字相互异或会发生什么呢?
57+
58+
```java
59+
a ⊕ b ⊕ a ⊕ b ⊕ c ⊕ c ⊕ d
60+
= ( a ⊕ a ) ⊕ ( b ⊕ b ) ⊕ ( c ⊕ c ) ⊕ d
61+
= 000 ⊕ d
62+
= d
63+
```
64+
65+
然后我们就找出了只出现了一次的数字。
66+
67+
这道题的话,因为要寻找的是两个数字,全部异或后不是我们所要的结果。介绍一下 [这里](https://leetcode.com/problems/single-number-iii/discuss/68900/Accepted-C%2B%2BJava-O(n)-time-O(1)-space-Easy-Solution-with-Detail-Explanations) 的思路。
68+
69+
如果我们把原数组分成两组,只出现过一次的两个数字分别在两组里边,那么问题就转换成之前的老问题了,只需要这两组里的数字各自异或,答案就出来了。
70+
71+
那么通过什么把数组分成两组呢?
72+
73+
放眼到二进制,我们要找的这两个数字是不同的,所以它俩至少有一位是不同的,所以我们可以根据这一位,把数组分成这一位都是 `1` 的一类和这一位都是 `0` 的一类,这样就把这两个数分到两组里了。
74+
75+
那么怎么知道那两个数字哪一位不同呢?
76+
77+
回到我们异或的结果,如果把数组中的所有数字异或,最后异或的结果,其实就是我们要找的两个数字的异或。而异或结果如果某一位是 `1`,也就意味着当前位两个数字一个是 `1` ,一个是 `0`,也就找到了不同的一位。
78+
79+
思路就是上边的了,然后再考虑代码怎么写。
80+
81+
怎么把数字分类?
82+
83+
我们构造一个数,把我们要找的那两个数字二进制不同的那一位写成 `1`,其它位都写 `0`,也就是 `0...0100...000` 的形式。
84+
85+
然后把构造出来的数和数组中的数字相与,如果结果是 `0`,那就意味着这个数属于当前位为 `0` 的一类。否则的话,就意味着这个数属于当前位为 `1` 的一类。
86+
87+
怎么构造 `0...0100...000` 这样的数。
88+
89+
由于我们异或得到的数可能不只一位是 `1`,可能是这样的 `0100110`,那么怎么只留一位是 `1` 呢?
90+
91+
方法有很多了。
92+
93+
比如,[201 题](https://leetcode.wang/leetcode-201-Bitwise-AND-of-Numbers-Range.html) 解法三介绍的 `Integer.highestOneBit` 方法,它可以保留某个数的最高位的 `1`,其它位全部置 `0`,源码的话当时也介绍了,可以过去看一下。
94+
95+
最后,总结下我们的算法,我们通过要找的两个数字的某一位不同,将原数组分成两组,然后组内分别进行异或,最后要找的数字就是两组分别异或的结果。
96+
97+
然后举个具体的例子,来理解一下算法。
98+
99+
```java
100+
[1,2,1,3,2,5]
101+
102+
1 = 001
103+
2 = 010
104+
1 = 001
105+
3 = 011
106+
2 = 010
107+
5 = 101
108+
109+
把上边所有的数字异或,最后得到的结果就是 3 ^ 5 = 6 (110)
110+
111+
然后对 110 调用 Integer.highestOneBit 方法就得到 100, 我们通过倒数第三位将原数组分类
112+
113+
倒数第三位为 0 的组
114+
1 = 001
115+
2 = 010
116+
1 = 001
117+
3 = 011
118+
2 = 010
119+
120+
倒数第三位为 1 的组
121+
5 = 101
122+
123+
最后组内数字依次异或即可。
124+
```
125+
126+
再结合代码,理解一下。
127+
128+
```java
129+
public int[] singleNumber(int[] nums) {
130+
int diff = 0;
131+
for (int n : nums) {
132+
diff ^= n;
133+
}
134+
diff = Integer.highestOneBit(diff);
135+
int[] result = { 0, 0 };
136+
for (int n : nums) {
137+
//当前位是 0 的组, 然后组内异或
138+
if ((diff & n) == 0) {
139+
result[0] ^= n;
140+
//当前位是 1 的组
141+
} else {
142+
result[1] ^= n;
143+
}
144+
}
145+
return result;
146+
}
147+
```
148+
149+
[这里](https://leetcode.com/problems/single-number-iii/discuss/69007/C-O(n)-time-O(1)-space-7-line-Solution-with-Detail-Explanation) 提出了一个小小的改进。
150+
151+
假如我们要找的数字是 `a``b`,一开始我们得到 `diff = a ^ b`。然后通过异或我们分别求出了 `a``b`
152+
153+
其实如果我们知道了 `a``b` 的话可以通过一次异或就能得到,`b = diff ^ a`
154+
155+
```java
156+
public int[] singleNumber(int[] nums) {
157+
int diff = 0;
158+
for (int n : nums) {
159+
diff ^= n;
160+
}
161+
int diff2 = Integer.highestOneBit(diff);
162+
int[] result = { 0, 0 };
163+
for (int n : nums) {
164+
//当前位是 0 的组, 然后组内异或
165+
if ((diff2 & n) == 0) {
166+
result[0] ^= n;
167+
}
168+
}
169+
result[1] = diff ^ result[0];
170+
return result;
171+
}
172+
```
173+
174+
得到只有一位 `1` 的数,除了 `Integer.highestOneBit` 的方法还有其他的做法。
175+
176+
[这里](https://leetcode.com/problems/single-number-iii/discuss/68900/Accepted-C%2B%2BJava-O(n)-time-O(1)-space-Easy-Solution-with-Detail-Explanations) 的做法。
177+
178+
```java
179+
diff &= -diff;
180+
```
181+
182+
取负号其实就是先取反,再加 `1`,需要 [补码](https://zhuanlan.zhihu.com/p/67227136) 的知识。最后再和原数相与就会保留最低位的 `1`。比如 `1010`,先取反是 `0101`,再加 `1`,就是 `0110`,再和 `1010` 相与,就是 `0010` 了。
183+
184+
还有 [这里](https://leetcode.com/problems/single-number-iii/discuss/68921/C%2B%2B-solution-O(n)-time-and-O(1)-space-easy-understaning-with-simple-explanation) 的做法。
185+
186+
```java
187+
diff = (diff & (diff - 1)) ^ diff;
188+
```
189+
190+
`n & (n - 1)` 的操作我们在 [191 题](https://leetcode.wang/leetcode-191-Number-of-1-Bits.html) 用过,它可以将最低位的 `1` 置为 `0`。比如 `1110`,先将最低位的 `1` 置为 `0` 就变成 `1100`,然后再和原数 `1110` 异或,就得到了 `0010`
191+
192+
还有 [这里](https://leetcode.com/problems/single-number-iii/discuss/68923/Bit-manipulation-beats-99.62) 的做法。
193+
194+
```java
195+
diff = xor & ~(diff - 1);
196+
```
197+
198+
先减 `1`,再取反,再相与。比如 `1010``1` 就是 `1001`,然后取反 `0110`,然后和原数 `1010` 相与,就是 `0010` 了。
199+
200+
还有 [这里](https://leetcode.com/problems/single-number-iii/discuss/342714/Best-Explanation-C%2B%2B) 的做法。
201+
202+
```java
203+
int mask=1;
204+
while((diff & mask)==0)
205+
{
206+
mask<<=1;
207+
}
208+
//mask 就是我们要构造的了
209+
```
210+
211+
这个方法比较直接,依次判断哪一位是 `1`
212+
213+
#
214+
215+
解法一的话经常用了,最容易想到的方法。
216+
217+
解法二的话,将问题转换成基本问题,这个思想经常用到,但有时候也比较难想。后边总结的得到只包含一个 `1` 的二进制的各种骚操作比较有意思。

0 commit comments

Comments
 (0)