给定一个整数数组 nums,其中恰好有两个元素只出现一次,其余所有元素均出现两次。 找出只出现一次的那两个元素。
示例 :
输入: [1,2,1,3,2,5]
输出: [3,5]
注意:
结果输出的顺序并不重要,对于上面的例子, [5, 3] 也是正确答案。
你的算法应该具有线性时间复杂度。你能否仅使用常数空间复杂度来实现?
方法一:哈希表
建立一个值到频率的映射关系的哈希表,返回频率为 1 的数字。
算法:
from collections import Counter
class Solution:
def singleNumber(self, nums: int) -> List[int]:
hashmap = Counter(nums)
return [x for x in hashmap if hashmap[x] == 1]
复杂度分析
时间复杂度:O(N)。
空间复杂度:O(N),哈希表所使用的空间。
方法二:两个掩码
本文将使用两个按位技巧:
使用异或运算可以帮助我们消除出现两次的数字;我们计算 bitmask ^= x,则 bitmask 留下的就是出现奇数次的位。
x & (-x) 是保留位中最右边 1 ,且将其余的 1 设位 0 的方法。
首先计算 bitmask ^= x,则 bitmask 不会保留出现两次数字的值,因为相同数字的异或值为 0。
但是 bitmask 会保留只出现一次的两个数字(x 和 y)之间的差异。
我们可以直接从 bitmask 中提取 x 和 y 吗?不能,但是我们可以用 bitmask 作为标记来分离 x 和 y。
我们通过 bitmask & (-bitmask) 保留 bitmask 最右边的 1,这个 1 要么来自 x,要么来自 y。
当我们找到了 x,那么 y = bitmask^x。
class Solution:
def singleNumber(self, nums: int) -> List[int]:
# difference between two numbers (x and y) which were seen only once
bitmask = 0
for num in nums:
bitmask ^= num
# rightmost 1-bit diff between x and y
diff = bitmask & (-bitmask)
x = 0
for num in nums:
# bitmask which will contain only x
if num & diff:
x ^= num
return [x, bitmask^x]
我们之前做过 136 题 ,当时是所有数字都是成对出现的,只有一个数字是落单的,找出这个落单的数字。其中介绍了异或的方法,把之前的介绍先粘贴过来。
还记得位操作中的异或吗?计算规则如下。
0 ⊕ 0 = 0
1 ⊕ 1 = 0
0 ⊕ 1 = 1
1 ⊕ 0 = 1
总结起来就是相同为零,不同为一。
根据上边的规则,可以推导出一些性质
0 ⊕ a = a
a ⊕ a = 0
此外异或满足交换律以及结合律。
所以对于之前的例子 a b a b c c d ,如果我们把给定的数字相互异或会发生什么呢?
a ⊕ b ⊕ a ⊕ b ⊕ c ⊕ c ⊕ d
= ( a ⊕ a ) ⊕ ( b ⊕ b ) ⊕ ( c ⊕ c ) ⊕ d
= 0 ⊕ 0 ⊕ 0 ⊕ d
= d
然后我们就找出了只出现了一次的数字。
这道题的话,因为要寻找的是两个数字,全部异或后不是我们所要的结果。介绍一下 这里 的思路。
如果我们把原数组分成两组,只出现过一次的两个数字分别在两组里边,那么问题就转换成之前的老问题了,只需要这两组里的数字各自异或,答案就出来了。
那么通过什么把数组分成两组呢?
放眼到二进制,我们要找的这两个数字是不同的,所以它俩至少有一位是不同的,所以我们可以根据这一位,把数组分成这一位都是 1 的一类和这一位都是 0 的一类,这样就把这两个数分到两组里了。
那么怎么知道那两个数字哪一位不同呢?
回到我们异或的结果,如果把数组中的所有数字异或,最后异或的结果,其实就是我们要找的两个数字的异或。而异或结果如果某一位是 1,也就意味着当前位两个数字一个是 1 ,一个是 0,也就找到了不同的一位。
思路就是上边的了,然后再考虑代码怎么写。
怎么把数字分类?
我们构造一个数,把我们要找的那两个数字二进制不同的那一位写成 1,其它位都写 0,也就是 0…0100…000 的形式。
然后把构造出来的数和数组中的数字相与,如果结果是 0,那就意味着这个数属于当前位为 0 的一类。否则的话,就意味着这个数属于当前位为 1 的一类。
怎么构造 0…0100…000 这样的数。
由于我们异或得到的数可能不只一位是 1,可能是这样的 0100110,那么怎么只留一位是 1 呢?
方法有很多了。
比如,201 题 解法三介绍的 Integer.highestOneBit 方法,它可以保留某个数的最高位的 1,其它位全部置 0,源码的话当时也介绍了,可以过去看一下。
最后,总结下我们的算法,我们通过要找的两个数字的某一位不同,将原数组分成两组,然后组内分别进行异或,最后要找的数字就是两组分别异或的结果。
然后举个具体的例子,来理解一下算法。
[1,2,1,3,2,5]
1 = 001
2 = 010
1 = 001
3 = 011
2 = 010
5 = 101
把上边所有的数字异或,最后得到的结果就是 3 ^ 5 = 6 (110)
然后对 110 调用 Integer.highestOneBit 方法就得到 100, 我们通过倒数第三位将原数组分类
倒数第三位为 0 的组
1 = 001
2 = 010
1 = 001
3 = 011
2 = 010
倒数第三位为 1 的组
5 = 101
最后组内数字依次异或即可。
def singleNumber2(self, nums: int):
# difference between two numbers (x and y) which were seen only once
mask=0
for num in nums:
mask^=num
temp=1
res=[0,0]
if (mask & 1)==1:
pass
else:
temp=temp+1
for y in nums:
if ((y & temp)==0):
res[0]^=y
else:
res[1]^=y
return(res)
进一步修改一下:
这里 提出了一个小小的改进。
假如我们要找的数字是 a 和 b,一开始我们得到 diff = a ^ b。然后通过异或我们分别求出了 a 和 b 。
其实如果我们知道了 a,b 的话可以通过一次异或就能得到,b = diff ^ a 。
public int[] singleNumber(int[] nums) {
int diff = 0;
for (int n : nums) {
diff ^= n;
}
int diff2 = Integer.highestOneBit(diff);
int[] result = { 0, 0 };
for (int n : nums) {
//当前位是 0 的组, 然后组内异或
if ((diff2 & n) == 0) {
result[0] ^= n;
}
}
result[1] = diff ^ result[0];
return result;
}