上一章我们认识了按位操作符, 这一章我们整理一下按位运算的经典案例
回顾
使用按位操作符的数,会先转成 32 位比特序列,也就是32 位的有符号的整数
如果这个数是正数,如果大于 ,只会保留低 32 位, 高于 32 位的数不存储;
如果这个数是负数,如果小于 ,只会保留低 32 位, 高于 32 位的数不存储;
按位异或概述
两个二进制数, 它们对应位的数只有 1 个 1, 结果为 1, 否则, 结果为 0。
性质
- 自反性:
a ^ b ^ b = a
- 交换律:
a ^ b = b ^ a
- 结合律:
a ^ (b ^ c) = (a ^ b) ^ c
- 恒等律:
a ^ 0 = a
- 归零律:
a ^ a = 0
浮点数转整数
12.12 ^ 0 //12
Math.PI ^ 0 //3
JavaScript 默认将数字存储为 64 位浮点数,但按位运算都是以 32位的二进制整数执行。
两个相同的数按位运算后, 结果只会保留整数部分,小数部分不存储。
Infinity ^ 0 //0
Number.MAX_VALUE ^ 0 //0
如果一个数大于 , 按位运算时只会保留低 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, 根据自反性得知,结果为 a,a 赋值给 b
a = a ^ b; // 分解开就是 a ^ (a ^ b), 根据自反性得知,结果为 b,b 赋值给 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)。
我们把这一题拆分成上面的那道题就好做了,拆分成两个数组分别异或。
- 把两个不同的数拆分到两个数组中。
- 把两个相同的数放到同一个数组中。
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
- 根据异或自反性可得出,数组所有数异或的结果是
x = a ^ b
- a 和 b是不相同的数,
a ^ b
的结果x
肯定不为 0,那么至少有 1 位是 1, 我们找出是哪一位?就可以分组了。 - 我们定义一个
mark=1
,mark 会左移, 根据x & mark
去找最近一位的 1,如果x & mark
等于 1, 找到位置,否则, mark 继续左移。 - 位移操作
mark
,我们得知mark
中只有 1 位是 1, 其他位都是 0, 所以遍历nums
判断n & mark
可得出: - 把结果是 1 的放到
xor1
组,否则,放到xor1
组。 - 相同的数
n & mark
的值是一样的, 肯定会放在同一个xor
中。
总结
- 两个相同的数,异或结果为 0
- 使用异或的自反性,可以交换两个数。
- 按位与和位移结合使用,可以判断标记位
- 用分治法把复杂问题简单化。