你真的懂异或运算吗

1,300 阅读4分钟

以下内容整理自左程云大佬的算法课。

异或运算

性质

性质:不同为1,相同为0,比如1001^0111 = 1110

更加有用的性质:看做无进位的二进制相加

这样看有一个好处,就是可以很方便的理解异或运算的一些拓展性质

拓展性质

0^N=N; N^N=0

很简单,如果看做无进位的二进制相加,0加上任何一个数都没有改变,而一个数加上自己,相当于多个(0+0=0和1+1=0但是不进位),所以最后结果就是0

满足交换律和结合律

因为相加满足交换律和结合律,所以异或运算当然也满足

同时我们在加异或多个数(无进位二进制相加多个数)时会发现:在一个位数上的结果,只和出现了多少个1有关

奇数个1就会让最后的结果是1,偶数个则会让最后的结果是0,因为两个1相加会导致一个0,而0没有影响结果

通过以上性质,我们可以用异或运算实现很多有趣的功能

功能

不用额外变量交换两个数

在以往,如果要交换两个数的值,必须使用一个新的变量

let a = 1, b = 2;
let temp; // 额外的临时变量
temp = a; // 把a的原始值先暂时保存起来以免被覆盖
a = b;    // 把b的原始值给a
b = temp  // 把b的值改为a被保存起来的原始值

其实有更加优雅的方法:

let a = 1, b = 2;
a = a ^ b; // 此时a的值是a^b
b = a ^ b; // 此时b的值是a^b^b = a
a = a ^ b; // 此时a的值是a^b^a = b

简单的来说就是用上文讲到的两个性质,因为N^N=0和0^N=N,所以第二步和第三步能得到a和b,最后能够实现结果的交换

找出数组中出现奇数次的数

如果在数组中只有一种数出现了奇数次,剩下的所有数都出现了偶数次,如何找出那个出现了奇数次的数

实际上解法很简单,不如设数组为这样 arr = [M,N,N,N,N,M,M]

把所有数异或起来,因为N^N=0,所以出现偶数次的数会两两抵消成为一个0不影响运算,而剩下的结果就是M^M^M,然后还是可以抽出偶数次,最后剩下M^0=M,所以最后的全部异或最后的答案就是出现奇数次的数

进阶:两个奇数次的数

那么如果只有两个数出现了奇数次,其他都是偶数次,又应该如何找到那两个出现了奇数次的数呢? 按照刚才的思路,假设那两个数是a和b,全部异或之后我们得到的应该是a^b

a一定不等于b,因为是两种数,所以a^b一定不是0,所以它的二进制位上一定有一个位是1

这说明a和b在那一位上一定是不同的,一个是1,一个是0

我们可以把数组中的数分为两类:一类是那一位是1的,一类是那一位是0的

我们可以随便异或一类,这样出现偶数次的数依然不会影响操作结果,我们就可以单独提出a或者b,而知道了a^b的结果和一个数(比如b),我们就可以通过a^b^b得到a

那么现在的关键是:如何得到一类数(某一位是1或者0)

方法就是:让a^b与自己的取反加1的结果进行与运算

比如一个数是011010,它的取反的结果是100101,然后加一的结果是100110

011010 & 100110 = 000010 ,这样就知道了011010这个数中最右边的1了

var oddTimesNum2(arr) {
    let eor = 0;                          //储存所有数异或结果
    for(let i = 0; i < arr.length; i++) {
        eor^= arr[i]
    }
                                          // eor上一定有一位是1
    let rightOne = eor & (~eor + 1);      // 提取一个不等于0的数中最右边的1
    let onlyOne = 0;                      // 另一个eor,只取那一位是1的数的异或结果
    for(let i = 0; i < arr.length; i++) {
        if((arr[i] & rightOne) != 0) {    // 只有那一位是1的数才会被计算(相当于滤波?)
            onlyOne ^= arr[i];
        }
    }
    return onlyOne,eor^onlyOne
}