|
| 1 | +# 题目描述(中等难度) |
| 2 | + |
| 3 | + |
| 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 | += 0 ⊕ 0 ⊕ 0 ⊕ 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