问题描述
http://www.lintcode.com/zh-cn/problem/single-number-ii/
给出3*n + 1 个的数字,除其中一个数字之外其他每个数字均出现三次,找到这个数字。
样例
给出 [1,1,2,3,3,3,2,2,4,1] ,返回 4
笔记
代码1
利用字典,存下来不同数字出现的次数,最后把次数1次的返回。时间复杂度O(n),空间复杂度O(n)。
代码2
排序,然后再找只出现一次的数。快排时间复杂度O(nlogn)(每次partition是n,平均需要logn次),空间复杂度O(1)。
代码3
利用一个数组记录二进制每个位的和。
代码4
用到了神奇的技巧。用三个变量,模拟三进制加法不进位。因为如果实现了三进制加法不进位,可以做到把3个重复的数去除,最后只剩下“落单的数”。
- one的某一位是1代表这一位是1,否则是0;
- two的某一位是1代表这一位是2,否则是0;
- mod是用来清零的,使用mod&one,mod&two,将one和two的某些位清0。
int one = 0;
int two = 0;
int mod = 0;
计算每一个数i的时候:
对于two
先判断有没有从one+i
那边进位过来的。有进位的情况是one&i
(对应的位都为1才会出现进位),将进位的情况与当前的two相加。有四种情况:
- 原来two的这一位是0,无进位,相当于0+0=0;
- 原来two的这一位是1,无进位,相当于2+0=2;这一位依然是1;
- 原来two的这一位是0,有进位,相当于0+2=2;这一位依然是1;
- 原来two的这一位是1,有进位,这是不可能的。有进位,说明原来one的对应位是1。现在,原来two的对应位也是1,我们早就应该把one和two进位清零了,不应该存在这种情况。
可见,这是一个或操作:two |= (one & i);
注意,two这一步用到了one,因此需要先算two,再算one,以防用了更新后的one。
对于one
使用异或。one ^= i;
即二进制加法不进位。
对于mod
如果one和two的某一位都是1,说明该进位清零了。使:
mod = ~(one & two)
如果one和two的某一位都是1,那么mod的这一位就是0,可以用这个0与one相与,将one的这一位清0:
one &= mod;
同理,对two也是一样
two &= mod;
除了上面那种情况,都是不需要进位的。这样mod的对应位就是1,相与之后保留one和two的相应位。
返回值
为啥是返回one呢?为啥不管two了?
因为题目中是只有一个数落单,其他都是三个三个的,在我们这个三进制加法不进位中全部抵消掉,结果是0,最后只会剩下那个落单的数,这个落单的数与0做”三进制加法不进位“,结果只会出现在one中。最后two必定为0。
举个例子把上面的流程跑一遍,加深理解。
[3,1,2]
初始化one=0,two=0,mod=0
加入3,0x11,two=0,one=0x11, mod=0x11, 不对one和two清零,one=0x11, two=0,看到one=0x11, two=0,已经把3纳入到我们的系统中了,结果是对的。
加入1,0x01。
对于two,two |= (one&1)
将1和one相与,0x01&0x11=0x01, 发现第0位进位了,因此要把这一位保存到two,two|=0x01, two = 0x01;
对于one,one ^= 1
将1与one做异或,0x01^0x11=0x10。
对于mod,mod = ~(one&two)
,因为one(0x10)和two(0x01)没有对应位都是1的,因此不需要进位清零,mod=0x11。
最终one=0x10,two=0x01。结果也是正确的。(3+1=4,在三进制的表示为0x11,正好是one+two(1*2+2*1=4))
加入3,0x11。
对于two,two |= (one&0x11)
将3和one相与,0x11&0x10=0x10, 发现第1位进位了,因此要把这一位保存到two,two|=0x10, two = 0x11;
对于one,one ^= 0x11
将3与one做异或,0x11^0x10=0x01。
对于mod,mod = ~(one&two)
,因为one(0x01)和two(0x11)的第0位都是1的,需要对第0位进位清零,mod=0x10。one&=mod
,one=0x00,two&=mod
, two=0x10
最终one=0x00,two=0x10。结果,(3+1+3=7,在三进制的表示为0x21,one=0x00,two=0x10,也就是0+1*4=4,已经进位清零了一次)
注:由于在我们的方法中,我们把三进制运算拆得更小了,所以在这种情况下,真正的三进制还没出现进位清零,但我们的方法已经出现了一次进位清零(7-3=4)
加入3,0x11。
对于two,two |= (one&0x11)
将3和one相与,0x11&0x00=0x00, 无进位,two|=0x00, two = 0x10;
对于one,one ^= 0x11
将3与one做异或,0x00^0x11=0x11。
对于mod,mod = ~(one&two)
,因为one(0x11)和two(0x10)的第1位都是1,需要对第1位进位清零,mod=0x01。one&=mod
,one=0x01,two&=mod
, two=0x00
最终one=0x01,two=0x00。最后的结果是正确的。
小结
经过实验表明,这种方法模拟的三进制运算并不是真正的三进制运算,而是把真正的三进制加法拆的更细了,不能保证中间的结果与真正的三进制相同。但是恰巧我们题目中用到的三进制特性是三个相同的数相加,每一位最后都必定进位,正好用上了这个特性,真是太高超的走火入魔的技巧。花了好多时间去理解,还是感觉没有完全理解,而且这个方法是适用面太窄了,还是留给大神们玩弄这种魔法吧。。
代码
代码1
class Solution {
public:
/**
* @param A : An integer array
* @return : An integer
*/
int singleNumberII(vector<int> &A) {
// write your code here
map<int, int> m;
for (int i : A)
{
if (m.find(i) != m.end())
m[i]++;
else
m[i] = 1;
}
for (auto i : m)
{
if (i.second == 1)
return i.first;
}
}
};
代码2
class Solution {
public:
/**
* @param A : An integer array
* @return : An integer
*/
int singleNumberII(vector<int> &A) {
// write your code here
sort(A.begin(), A.end());
if(A[0] != A[1])
return A[0];
if(A[A.size()-1] != A[A.size()-2])
return A[A.size()-1];
for (int i = 1; i < A.size() - 1;i++)
{
if (A[i-1] != A[i] && A[i] != A[i+1])
return A[i];
}
}
};
代码3
class Solution {
public:
/**
* @param A : An integer array
* @return : An integer
*/
void set(int &a, int i)
{
a |= (1 << (i & 0x1F));
}
int singleNumberII(vector<int> &A) {
// write your code here
int bits[32];
memset(bits, 0, sizeof(bits));
for (int i = 0; i < 32; i++)
{
for (auto j : A)
{
bits[i] += ((j >> i) & 1);
}
}
int res = 0;
for (int i = 0; i < 32; i++)
{
if (bits[i] % 3 != 0)
set(res, i);
}
return res;
}
};
代码4
class Solution {
public:
/**
* @param A : An integer array
* @return : An integer
*/
int singleNumberII(vector<int> &A) {
// write your code here
int one = 0;
int two = 0;
int mod = 0;
for (auto i : A)
{
two |= (i & one);
one ^= i;
mod = ~(one & two);
one &= mod;
two &= mod;
}
return one;
}
};