二进制的妙用

1,028 阅读6分钟

为了简洁,本文的二进制皆省略了 0d 开头,具体二进制表示以具体语言为准。

智力题

让我们从一个经典的面试智力题说起。

1.有1000瓶药,其中一瓶有毒。老鼠喝下,就会在一天后死亡。问最少要多少只老鼠才能在一天后知道哪一瓶有毒。

答案是 10 只老鼠,`2 ** 10 = 1024 > 1000`
  1. 我们可以把1000瓶药按二进制编号: 第一瓶是00 0000 0001、第二瓶是00 0000 0010... 到第一千瓶11 1110 1000

  2. 然后十只老鼠分别表示 0 到 9 号,第 0 号老鼠喝下所有药水编号中第 0 位为 1 的所有药水 、第 1 号老鼠喝下所有药水编号中第 1 位为 1 的所有药水,以次类推。

  3. 最后根据死掉的老鼠便能知道哪瓶药水有毒。如第0、5号老鼠死了,有毒的便是00 0001 0001,第17瓶。

整个过程类似于每个老鼠作为二进制的一位,让人不经联想到《三体》中’秦始皇‘派三千万人组成的人列计算机

扩展

2.有1000瓶药,其中一瓶有毒。老鼠喝下,就会在一天后死亡。问最少要多少只老鼠才能在两天后知道哪一瓶有毒。

答案是 7 只老鼠,`3 ** 7 = 2187 > 1000`

和上题的方法类似,不过要使用三进制。

  1. 我们可以把1000瓶药按三进制编号: 第一瓶是000 0001、第二瓶是000 0002... 到第一千瓶110 1001

  2. 第一天,七只老鼠分别表示 0 到 6 号,第 0 号老鼠喝下所有药水编号中第 0 位为 2 的所有药水 、第 1 号老鼠喝下所有药水编号中第 1 位为 2 的所有药水,...

  3. 在第二天可以根据死了老鼠的编号,断定该毒药的三进制该位是 2。第二天没死的老鼠继续喝该编号为 1 的药水,在第三天可以根据死了老鼠的编号,断定该毒药的三进制该位是 1

  4. 最后根据第一、二天死掉的老鼠便能知道哪瓶药水有毒。如第一天 0、5 号老鼠死了、第二天 1 号老鼠死了,有毒的便是002 0012,第167瓶。

singleNumber

1.找到数组中只出现一次的数字,其他数字都出现两次(偶数次)。

答案是异或 ^ 运算。

异或运算,即0 ^ 0 = 01 ^ 1 = 01 ^ 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 会得到 00。最后 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元二进制机械计数器正好符合格雷码计数器的计数规律。

在计算机网络中,数字基带信号为了携带更多的数据,通常使用调幅、调频、调相来改变波形。 Snipaste_2021-10-01_19-00-56.png 以混合调制-正交振幅调制QAM-16为例:

  • 12种相位
  • 每种相位有1或2种振幅可选
  • 一共可以调制出16种码元 用星座图表示如下: Snipaste_2021-10-01_19-04-51.png 如果用二进制定义以上码元,可知每个码元能携带四个比特。如0000,0001,那么四个比特和码元能随便定义吗?答案是不能,因为在数据传输中,信号通常会收到干扰而失真,调制出的码元解调后并不准确。如下随机定义的星座图:

Snipaste_2021-10-01_19-16-46.png

其中A、B、C、D、E五个码元本身都对应0000,但是由于信号失真,导致在星座图中并未落在理想位置。其中A、B、C能被解调为0000(正确),但D被解调为0001(一位错误),码元E被解调为1111(四位全错)。于是可知四个比特和码元不能随便定义,为了降低错误率,应该采用格雷码,也就是每个相邻码元只有一位不同,如下图:

Snipaste_2021-10-01_19-19-13.png

参考资料

编码与调制