题目:一个数组中一个数出现了p次,其余数出现了k次(p%k != 0),不用额外空间找出这一个数。
由于在电脑中,数是由位存储的,我们先考虑简单情况----只有一位的数(0或者1),我们对1进行计数,当计数器到达k时,归零, 设我们扫描数组遇到的数为i。假设这个计数器是一个m位的二进制数:xm, ..., x1。这个计数器有以下性质。
使用 x = x ^ 0 可以保证遇0计数器不变;
- 遇0不变,遇1增1
- 为了能计数到k,需要满足 2^m >= k
对于1,第一次遇到时, x1 = x1 ^ i , 计数器变成 xm = 0, ..., x2 = 0, x1 = 1;
第二次遇到1时,我们希望x2变成1, x1变成 0, 即为xm = 0, ... x2 = 1, x1 = 0 。对于x1来说仍然使用 x1 = x1 ^ i来改变状态, 对于x2来说只有x1和遇到的数同时为1时,才需要改变状态,所以使用x2 = x2 ^ (x1 & i); 同理对于xm, xm = xm ^ (xm-1 & ... & x1 & i)。
对于计数器,当2^m == k时,计数k个之后会自动归0;
当2^m > k时,我们自己设计让计数器计数k个之后回归0,为了实现这个效果,我们把计数器与mask做&操作,mask具有以下性质
- 只有当计数器到达k时,mask才为0,其余时刻为1.
把k写成m位的二进制数, km, ...,k2, k1, 我们的计数器为xm, ..., x2, x1, 构造mask如下
mask = ~(y1 & y2 & ... & ym), where yj = xj if kj = 1, and yj = ~xj if kj = 0 (j = 1 to m).
上面的mask保证了只有km == xm, ..., k1 == x1,即计数器等于k时,mask为0。例如:
k = 3时 2^m >= k, 所以 m = 2, k1 = 1, k2 = 1, mask = ~(x1 & x2)
k = 5: k1 = 1, k2 = 0, k3 = 1, mask = ~(x1 & ~x2 & x3);
对于位数为1的数,算法表达如下:
for (int i : nums) {
xm ^= (xm-1 & ... & x1 & i);
xm-1 ^= (xm-2 & ... & x1 & i);
.....
x1 ^= i;
mask = ~(y1 & y2 & ... & ym) where yj = xj if kj = 1, and yj = ~xj if kj = 0 (j = 1 to m).
xm &= mask;
......
x1 &= mask;
}
接下来讨论普遍情况,遇到的数是32位。那么我们就需要为32位构造32个计数器,我们通过m各32位的数就构成了32个m位的计数器。由于位操作是独立的,相互之间无影响,即32个计数器之间是独立的,所以这样构造是合法的。通过第r个计数器去对数的第r位的1进行计数。
最后要讨论的是,计数结束之后,怎么得到寻找的数。首先明确这m个数xm, ... x1的概念,xm, ... x1的第r位由所扫描数组中的数的第r位决定。注意到计数器到k会归0,那么有效的次数就是 p' = p % k。
将p'写成m位的二进制数, pm, ..., p1, 注意p'等于每一个counter。 结论是:如果pj == 1, 那么xj就是所寻找的数(xj是上图横着的32位的数)
证明:如果xj的第r位为1,那么所寻找的数第r位一定为1。 如果xj的第r位为0,如果所寻找的数第r位为1,由于pj = 1, 所以
xj的第r位也一定为1,矛盾;所以所寻找数的第r位为为0;综上当pj =1时, xj 等于所寻找的数。
注意:当pj = 0 时,当所寻找数第r位为1, xj的第r位为0, 当所寻找数的第r位为0时,xj的第r位为0, 所以当pj=0时, xj =0, 所以最后的答案也可以写成 x1 | x2 |... | xm
举例,k = 2, p = 1, 这时m = 1,需要一个计数器;2^m == k,计数器会自动归0,不需要mask
public int singleNumber(int[] nums) {
int x1 = 0;
for (int i : nums) {
x1 ^= i;
}
return x1;
}
k= 3, p = 1, 这时 m = 2, 需要两个计数器x2, x1; 2^m > k,需要mask,k2 = 1, k1 = 1, 所以 mask = ~(x1 & x2)
public int singleNumber(int[] nums) {
int x1 = 0, x2 = 0, mask = 0;
for (int i : nums) {
x2 ^= x1 & i;
x1 ^= i;
mask = ~(x1 & x2);
x2 &= mask;
x1 &= mask;
}
return x1;
}
本文参考,英文好的可以直接看英文:https://leetcode.com/problems/single-number-ii/discuss/43295/Detailed-explanation-and-generalization-of-the-bitwise-operation-method-for-single-numbers