今天偶然看到的有趣的问题,有好几个系列,感受位运算的小魅力。
当然下面的题可以用map来简单算出,所以下面的题要求时间复杂度为O(n),空间复杂度为O(1)。
1. 简单版
一个数组中,只有一个数只出现一次,其他的数都是成双成对出现(2次,4次,6次…),求这出现一次的数。比如{1,2,3,1,2}求的是3。
熟悉异或运算的朋友,看到就会说so easy了。
就是利用异或的性质,两个相同的数异或结果为0,不同为1,同时异或与顺序无关,即满足交换率。
因此利用这个性质,可以对该数组所有的数进行异或操作,则成双成对的数异或后结果为0,得到的值就是只出现一次的值。
实现如下:
int getUnique(int a[])
{
int ans = 0;
for(int item: a)
ans ^= item;
return ans;
}
2. 普通版
一个数组中,只有一个数只出现一次,其他的数都是出现3次,求这出现一次的数。如{1, 2, 1, 1, 2, 3, 2} 求的就是3。
由于不再是成双成对,因此直接异或怕是解决不了了。不怕,还是可以用位运算来计算的,考虑的是二进制的表示,因为除了一个数,其他每个数都出现了3次,所以在这一位上对所有数求和后对3取余后,出现3次的数对3取余自然为0了,所以结果就是出现一次的数在这一位的值,所以只要累加就可以得到结果了。
说这么多废话,直接上个例子比较直观。
{1, 2, 1, 1, 2, 3, 2} 计算过程如下:
0001
0010
0001
0001
0010
0011
+ 0010
------
0044
% 3333
------
0011 =》 3
看懂上面过程就很明确了,这里int为4个字节即32位。实现代码也很简单,如下:
int getUnique(int a[])
{
for(int item: a){
for(int i=0; i<32; i++){
msum[i]+=(item>>i&1); // 每个位的累加和
}
}
int ans = 0;
for(int i=0; i<32; i++){
ans += (sum[i]<<i)%3; // 注意每一位上的和要对3取余
}
return ans;
}
3. 升级版
一个数组中,只有两个数只出现一次,其他的数都是成双成对出现(2次,4次,6次…),求这出现一次的两个数。比如{1,4,3,1,5,5}求的就是4和3。
- 似曾相熟,之前是只有一个数只出现一次,现在是两个数,同样的我们还是会将所有的数异或,显然最后得到的是这两个数的异或值。
那么如何利用这个数的异或值来得到两个确切的值呢? - 如果能够把原数组分为两个子数组。在每个子数组中,包含一个只出现一次的数字,而其他数字都是成双成对的。如果能够这样拆分原数组,按照前面的办法就是分别求出这两个只出现一次的数字了。
- 这样考虑,最后得到的两个不同的数的异或值,那这个异或值肯定不为0,因此可以以这个异或值最高位的1(计为第x位)分组,即将所有的数分成两个组,一组是x位是0的集合,另一组是x位是1的集合,这样这个数就分到了两组,成双成对的数也被分到了唯一的一组。
- 因此现在就可以处理了,将这个异或值与其中一组异或,那么在这组出现一次的数就变成双成对,所以得到的值就是另一个数。同样对另一组异或得到另一个数。
废话还是有点多,还是用个例子介绍:
{1,4,3,1,5,5}
1^4^3^1^5^5 = 4^3 = 7 = 0111(2进制)
所以以第三位的值分成两组:
「第三位为0的分组」:{1,3,1}
「第三位为1的分组」:{4,5,5}
第一个数为: 7^1^3^1 = 4^3^1^3^1 = 4
第二个数为: 7^4^5^5 = 4^3^4^5^5 = 3
代码如下:
// 找出x第一个1出现的位置
int findFirstBit(int x)
{
int indexBit = 0;
while((x & 1 == 0) && (indexBit < 32)){
x = x >> 1;
indexBit++;
}
return indexBit;
}
void solve(int a[])
{
int k = 0;
for(int item: a)
k ^= item; // 得到的是两个出现一次的值的异或值
int indexBit = findFirstBit(k); // 第一个1出现的位
int ans1 = k, ans2 = k; // ans1和ans2分别对应两个只出现一次的数
for(int item: a){
if((item>>indexBit) & 1 == 0)
ans1 ^= item;
else
ans2 ^= item;
}
}