以下内容整理自左程云大佬的算法课。
异或运算
性质
性质:不同为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
}