按位操作符-按位异或(XOR)

724 阅读5分钟

上一章我们认识了按位操作符, 这一章我们整理一下按位运算的经典案例

带你认识按位操作符

回顾

使用按位操作符的数,会先转成 32 位比特序列,也就是32 位的有符号的整数

如果这个数是正数,如果大于 23112^{31}-1,只会保留低 32 位, 高于 32 位的数不存储;

如果这个数是负数,如果小于 231-2^{31},只会保留低 32 位, 高于 32 位的数不存储;

按位异或概述

两个二进制数, 它们对应位的数只有 1 个 1, 结果为 1, 否则, 结果为 0。

性质

  1. 自反性: a ^ b ^ b = a
  2. 交换律: a ^ b = b ^ a
  3. 结合律:a ^ (b ^ c) = (a ^ b) ^ c
  4. 恒等律: a ^ 0 = a
  5. 归零律: a ^ a = 0

浮点数转整数

12.12 ^ 0   //12
Math.PI ^ 0  //3

JavaScript 默认将数字存储为 64 位浮点数,但按位运算都是以 32位的二进制整数执行。

两个相同的数按位运算后, 结果只会保留整数部分,小数部分不存储。

Infinity ^ 0 //0
Number.MAX_VALUE ^ 0 //0

如果一个数大于 23112^{31}-1, 按位运算时只会保留低 32 位运算, 高于 32位的数丢弃, 结果就不准确了。

两个相同的数异或为 0

3 ^ 3           //0
12.12 ^ 12.12   //0

不用临时变量,交换两个整数的值

let a = 3, b =5;
a = a ^ b;
b = a ^ b;  // 分解开就是 (a ^ b) ^ b, 根据自反性得知,结果为 aa 赋值给 b
a = a ^ b;  // 分解开就是 a ^ (a ^ b), 根据自反性得知,结果为 bb 赋值给 a

console.log(a, b)  // 5, 3

我们画图看一下:

 a = 3 (base 10) = 00000000000000000000000000000011 (base 2)
 b = 5 (base 10) = 00000000000000000000000000000101 (base 2)
                   --------------------------------
 3 ^ 5 (base 10) = 00000000000000000000000000000110 (base 2) = 6 (base 10) = a
 
 //现在 a = 6了
 a = 6 (base 10) = 00000000000000000000000000000110 (base 2)
 b = 5 (base 10) = 00000000000000000000000000000101 (base 2)
                   --------------------------------
 6 ^ 5 (base 10) = 00000000000000000000000000000011 (base 2) = 3 (base 10) = b
 
 //现在 b = 3 了, 已经把 a 的值交换给 b 了
 a = 6 (base 10) = 00000000000000000000000000000110 (base 2)
 b = 3 (base 10) = 00000000000000000000000000000011 (base 2)
                   --------------------------------
 6 ^ 3 (base 10) = 00000000000000000000000000000101 (base 2) = 5 (base 10) = a

经过三次 ^操作后, 我们把 a 和 b的值交换成功了。

找出重复的数

1-1000放在含有1001个元素的数组中,只有唯一的一个元素重复,找出这个重复的数字。

设重复的数为 x, 根据自反性可以得到:
(1 ^ 2 ...... 999 ^ 1000)^ (1 ^ 2 ...... 999 ^ 1000 ^ x) = x

找唯一不同的数

找出唯一一个在数组中出现一次的整数,而其他都会出现两次。

我们知道两个相同的数异或为 0, 那么把数组中的所有整数异或运算, 剩下的就是唯一出现一次的数。

let arr = [1,2,1,2,3]
let value = arr.reduce((p, c) => p ^ c)
console.log(value)    //3

找出不同的两个数

一个整型数组 nums 里除两个数字之外,其他数字都出现了两次。请写程序找出这两个只出现一次的数字。要求时间复杂度是O(n),空间复杂度是O(1)。

我们把这一题拆分成上面的那道题就好做了,拆分成两个数组分别异或。

  1. 把两个不同的数拆分到两个数组中。
  2. 把两个相同的数放到同一个数组中。
const nums = [1, 4, 3, 4]
const x = nums.reduce((p, c) => p ^ c)    //x = a ^ b 
let mark = 1;
//两个不相同的数, 异或对应位的数字至少有 1 位不相同, 我们找出这个数.
while((x & mark) === 0) mark <<= 1; 
let xor1 = 0;
let xor2 = 0;

for (let n of nums) {
    if(n & mark){   
        xor1 ^= n;
    }else{
        xor2 ^= n;
    }
}

console.log(xor1, xor2) //3,1

设 两个不同的数是 a 和 b

  1. 根据异或自反性可得出,数组所有数异或的结果是 x = a ^ b
  2. a 和 b是不相同的数, a ^ b的结果x肯定不为 0,那么至少有 1 位是 1, 我们找出是哪一位?就可以分组了。
  3. 我们定义一个 mark=1 ,mark 会左移, 根据 x & mark去找最近一位的 1,如果 x & mark等于 1, 找到位置,否则, mark 继续左移。
  4. 位移操作mark,我们得知mark中只有 1 位是 1, 其他位都是 0, 所以遍历nums判断n & mark可得出:
  5. 把结果是 1 的放到 xor1 组,否则,放到xor1组。
  6. 相同的数 n & mark的值是一样的, 肯定会放在同一个 xor中。

总结

  1. 两个相同的数,异或结果为 0
  2. 使用异或的自反性,可以交换两个数。
  3. 按位与和位移结合使用,可以判断标记位
  4. 用分治法把复杂问题简单化。