为了简洁,本文的二进制皆省略了
0d开头,具体二进制表示以具体语言为准。
智力题
让我们从一个经典的面试智力题说起。
1.有1000瓶药,其中一瓶有毒。老鼠喝下,就会在一天后死亡。问最少要多少只老鼠才能在一天后知道哪一瓶有毒。
答案是 10 只老鼠,`2 ** 10 = 1024 > 1000`。
-
我们可以把1000瓶药按二进制编号: 第一瓶是
00 0000 0001、第二瓶是00 0000 0010... 到第一千瓶11 1110 1000。 -
然后十只老鼠分别表示 0 到 9 号,第 0 号老鼠喝下所有药水编号中第 0 位为
1的所有药水 、第 1 号老鼠喝下所有药水编号中第 1 位为1的所有药水,以次类推。 -
最后根据死掉的老鼠便能知道哪瓶药水有毒。如第0、5号老鼠死了,有毒的便是
00 0001 0001,第17瓶。
整个过程类似于每个老鼠作为二进制的一位,让人不经联想到《三体》中’秦始皇‘派三千万人组成的人列计算机。
扩展
2.有1000瓶药,其中一瓶有毒。老鼠喝下,就会在一天后死亡。问最少要多少只老鼠才能在两天后知道哪一瓶有毒。
答案是 7 只老鼠,`3 ** 7 = 2187 > 1000`。
和上题的方法类似,不过要使用三进制。
-
我们可以把1000瓶药按三进制编号: 第一瓶是
000 0001、第二瓶是000 0002... 到第一千瓶110 1001。 -
第一天,七只老鼠分别表示 0 到 6 号,第 0 号老鼠喝下所有药水编号中第 0 位为
2的所有药水 、第 1 号老鼠喝下所有药水编号中第 1 位为2的所有药水,... -
在第二天可以根据死了老鼠的编号,断定该毒药的三进制该位是
2。第二天没死的老鼠继续喝该编号为1的药水,在第三天可以根据死了老鼠的编号,断定该毒药的三进制该位是1。 -
最后根据第一、二天死掉的老鼠便能知道哪瓶药水有毒。如第一天 0、5 号老鼠死了、第二天 1 号老鼠死了,有毒的便是
002 0012,第167瓶。
singleNumber
1.找到数组中只出现一次的数字,其他数字都出现两次(偶数次)。
答案是异或 ^ 运算。
异或运算,即0 ^ 0 = 0、1 ^ 1 = 0、 1 ^ 0 = 1。任何一个数字和自己异或的结果是 0 ,任何一个数字和 0 的异或都是本身。
那么我们可以大胆点,将数组里所有的数字进行异或操作,最后的结果也就是只出现一次的数字。如数组[3, 3, 1] , 3 ^ 3 ^ 1 = 1;
扩展
2.找到数组中只出现一次的数字,其他数字都出现三次(奇数次)。
三次的情况会比较复杂,通常的做法是利用Map保存出现的次数,然后再遍历Map。当然,二进制也是能够实现的:
考虑[5, 5, 5, 1]
1 0 1
1 0 1
1 0 1
0 0 1
把数字理解为二进制,每一位求和得:
3 0 4
再每位 %3 得:
0 0 1
js 实现如下:
/**
* @param {number[]} nums
* @return {number}
*/
function singleNumber(nums) {
let res = 0;
for (let i=0; i<32; i++) {
let cnt = 0;
let bit = 1 << i;
nums.forEach(val => {
if (val & bit) cnt++;
})
if (cnt%3 != 0) res = res | bit;
}
return res;
};
结束了吗?力扣:只出现一次的数字 II
我们可以参考二进制计算的思想,用 a、b 来统计数字 n 出现的次数,得到:
初始值 第一次出现 第二次出现 第三次出现
a 0 0 n 0
b 0 n 0 0
这样计算完出现三次的数字,a、b 会得到 0、0。最后 b 的值即为只出现一次的数字。
js 实现如下(& ~ 可理解为剔除操作,参考3.鉴权):
/**
* @param {number[]} nums
* @return {number}
*/
function singleNumber(nums) {
let a = 0, b = 0;
for (let num of nums) {
b = (b ^ num) & ~a;
a = (a ^ num) & ~b;
}
return b;
};
鉴权
我们可以利用二进制保存用户的权限,如
添加 0001
修改 0010
删除 0100
查询 1000
这样做的好处是,如果有多项权限可以直接 | 运算。如同时拥有 [添加、修改] 权限就是0001 | 0010 结果为 0011。
在查询权限时可以直接 & 运算。如是否有 [添加] 权限 if (0011 & 0001) { }。
在移除权限时可以进行 & ~ 运算。如移除 [添加] 的权限 0011 & ~0001。
如在React中便是通过二进制表示effectTag,可以方便的使用位操作赋值多个effect。
// DOM需要插入到页面中
export const Placement = /* */ 0b00000000000010;
// DOM需要更新
export const Update = /* */ 0b00000000000100;
// DOM需要插入到页面中并更新
export const PlacementAndUpdate = /* */ 0b00000000000110;
// DOM需要删除
export const Deletion = /* */ 0b00000000001000;
// 初始化,没有effect
fiber.effectTag = NoEffect;
// 标记Update
fiber.effectTag |= Update;
// 标记Placement,该fiber同时有Update与Placement标记
fiber.effectTag |= Placement;
// 删除 Placement
fiber.effectTag &= ~Placement;
// 判断是否有 Placement标记
(fiber.effectTag & Placement) === NoEffect ? false : true;
格雷码
典型的二进制格雷码(Binary Gray Code)简称格雷码,因1953年公开的弗兰克·格雷(Frank Gray,18870913-19690523)专利“Pulse Code Communication”而得名,当初是为了通信,现在则常用于模拟-数字转换和位置-数字转换中。法国电讯工程师波特(Jean-Maurice-Émile Baudot,18450911-19030328)在1880年曾用过的波特码相当于它的一种变形。1941年George Stibitz设计的一种8元二进制机械计数器正好符合格雷码计数器的计数规律。
在计算机网络中,数字基带信号为了携带更多的数据,通常使用调幅、调频、调相来改变波形。
以混合调制-正交振幅调制QAM-16为例:
- 12种相位
- 每种相位有1或2种振幅可选
- 一共可以调制出16种码元
用星座图表示如下:
如果用二进制定义以上码元,可知每个码元能携带四个比特。如
0000,0001,那么四个比特和码元能随便定义吗?答案是不能,因为在数据传输中,信号通常会收到干扰而失真,调制出的码元解调后并不准确。如下随机定义的星座图:
其中A、B、C、D、E五个码元本身都对应0000,但是由于信号失真,导致在星座图中并未落在理想位置。其中A、B、C能被解调为0000(正确),但D被解调为0001(一位错误),码元E被解调为1111(四位全错)。于是可知四个比特和码元不能随便定义,为了降低错误率,应该采用格雷码,也就是每个相邻码元只有一位不同,如下图: