找到唯一的那个数——对于异或运算的研究

异或运算,具有一种很特殊的性质,这种性质使得它成为了现代加密学的核心运算。这个特性很简单,那就是“排同求异”,凡是相同的两个值参加运算,结果就是0. 那么很明显就有x XOR 0 == x. 

而另一方面,我们知道,x XOR k XOR k = x,同时可以看到,如果令y=x XOR k, 那么在不知道k的具体值的时候,几乎无法从y推测出x的值,这样x就被加密了,为了使得y变得更加无法理解,毫无规律而言,甚至可以用随机数生成器来生成k, 这样的k就是密钥,除非被泄露,否则根本无法破解。


那么,这和找到唯一数有什么关系呢?事实上,网上有人分析了那道题目的解法,但是仅仅是贴出了源代码,毫无任何解释:

int singleNumber(int A[], int n)  
{  
    int one = 0, two = 0;  
    for (int i = 0; i < n; i++)  
    {  
        two |= A[i] & one;//two 积累值  
        one ^= A[i];//<del><span style="color:#ff0000;">one不断求反</span></del>  
        int t = one & two;//第三次的时候one和two都保留了该位的值  
        one &= ~t;//清零出现三次的该位的值  
        two &= ~t;  
    }  
    return one;  
}  

这种像天书一样的玩意,这样看一看源码就能理解了?!所以,很有必要去从这一现象的数学本质上去加以理解,这就是为什么我们要在这里花时间讨论异或运算的原因。


为了简单起见,我们首先研究每个数重复2次的情况。思路很简单,把每个数在内存中的形态想象出来,没错就是二进制展开,这样每个数占一行,把整个数组展现出来,就会像一个矩阵一样。现在,来分别看每一位(每一列),因为我们知道,每一列中,除去要找的那个数所占的行,所有的0和1必定是成对出现的。那么,根据异或运算的特性,以及交换律,很容易发现,这一列所有的值进行异或以后,剩下的值就是那个唯一数对应位所应有的值:

x1 XOR x2 XOR ... XOR x* XOR ... xn = x1 XOR x2 XOR  ... XOR x* = 0 XOR x* = x*

所以,这样的情形很容易求解:

public int singleNumber(int[] A) {
        int ret = 0;
        for(int i=0;i<A.length;i++) ret ^= A[i];
        return ret;
    }

好了,一些人很天真的认为,这样的算法一定也可以用到重复3次的情况吧?很遗憾,事情没有这么简单,举个例子:

2: 10

3: 11

2: 10

2: 10

2 XOR 3 XOR 2 XOR 2 = 1 != 3

写出这个式子后,很快就会发现问题出在哪了:1 XOR 1 XOR 1 = 0 XOR 1 = 1 != 0

看起来,我们似乎不能用异或了,那么,尝试自己发明一种新的运算呢?。。。如果你尝试了,那么很快就会发现矛盾,从而这样做并不可取:

定义新运算 @

那么我首先希望这个运算具有性质: 1 @ 1 @ 1 = 0

好了,那么1 @ 1 = ?

如果 1@1=1 那么上面那个表达式就不成立了,所以1@1=0。但是这样以来就有0@1=0, 从而 0@0=0. 这样,不论什么数,经过这个运算符计算后都变成了0.很明显,这不是我们所期望的。


但是,我们的初衷看上去确实正确的,那就是,必须要让1 @ 1 @ 1 = 0。换句话说,原来的算法之所以不能用,就是因为1 XOR 1 XOR 1 = 1了。所以,如果有方法能在第三次对1做异或运算的时候,把对应位清0,原来的算法就又可以使用了!而我们之前尝试去定义新的运算符,也真是为了如此。现如今,定义运算符是不可能了,这说明我们只能想办法去保留运算次数的信息,然后手工在第三次运算结束后清0. 这就是最上面那个天书一般算法的核心思想:


one代表一次标记,它记录了,从上一次清0到当前运算开始前,每一个对应位上,1出现了一次的位置;

two代表二次标记,它记录了,从上一次清0到当前运算开始前,每一个对应位上,1出现了两次的位置;


所以two |= A[i] & one 就是更新two的标记值,用已经出现的1的一次标记,加上再次读入的新信息,就可以推算1的二次标记;

one ^= A[i] 这个就是异或运算,因为一次标记中所有的1代表的就是当前位从上一次清0到此次运算开始前,1只出现了一次的位置。那么,对于one标记中0的解释,就有两种情况了:一种是,上一次清0发生在读到三个1的时候,所以,现在出现的1确实是首次出现的,对应的two标记中的值是0;另一种就是,上一次清0发生在读到两个1做了异或运算以后,所以,对应的two标记中的值是1。注意到对于后者,现在再出现的1就是第三次出现,必须要清0. 所以 one ^= A[i] 以后,一些位是1,但是这些位对应的解释是不同的,只有那些真的是第三次出现的才需要被处理,而这个信息,我们向two标记索取:

int t = one & two;

然后,知道对应需要清0的位以后,所需做的,就是清0了:
one &= ~t;
two &= ~t;

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值