🧮 第8章 算法和计算机
📄 一句话概括

十进制转二进制:除二取余,余数从下往上读。 二进制转十进制:按位权展开,只把 1 的那几位加起来,权重是 128 64 32 16 8 4 2 1。 小数部分反过来:乘二取整,从上往下读。方向相反,这是最容易记混的一处。

📝 本章和前面几章的关系

上一篇认写法,这一篇讲换算。两个方向各一套动作,都不难,但要练熟。 教材用 173 当例子,因为它正好八位、二进制里 0 和 1 都有,看得清。 最后一节会说到 0.1 存不下——那是「小数部分转换」最实际的后果。

一、十进制转二进制:除二取余

把 173 一直除以 2,每次记下余数:173 除 2 商 86 余 1;86 除 2 商 43 余 0;一直除到商是 0 为止。 八行除完,余数从上到下是 1、0、1、1、0、1、0、1。 从下往上读,得到 10101101。

为什么从下往上读?因为先除出来的余数是最低位(个位对应的那一位),最后除出来的才是最高位。 顺序反了,数就全错了。而且反过来的结果看着也很像样——不会报错,只是错,所以这个方向必须记牢。

二、反过来:按位权展开

10101101 一共 8 位,从左边开始每一位的权重是 128、64、32、16、8、4、2、1。 只把「这一位是 1」的那几个加起来:128 + 32 + 8 + 4 + 1 = 173。

位 权重 这一位是 1 吗 加起来
1 128 1 128
2 64 0 不加
3 32 1 32
4 16 0 不加
5 8 1 8
6 4 1 4
7 2 0 不加
8 1 1 1

这个方法不用记公式,只需要认权重。记住那八个数,八位二进制就能心算。 权重是怎么来的?每一位是 2 的幂:从右往左数第 0 位是 1,第 1 位是 2,第 7 位是 128。 这就是「位权」两个字的意思。 这也是第 2.4 讲数组地址能用乘法算出来的同一个道理——位置决定权重,权重决定贡献多少。

三、小数部分:乘二取整

整数部分除二取余,小数部分反过来:每次乘 2,取出整数部分,剩下的小数继续乘。 0.625 乘 2 得 1.25,取 1,剩 0.25;0.25 乘 2 得 0.5,取 0,剩 0.5;0.5 乘 2 得 1.0,取 1,剩 0。 停。 整数部分从上往下读,得到 0.101。

验算一下:0.101 对应 1/2 加 1/8,也就是 0.5 加 0.125,正好 0.625。 所以两个方向的动作是相反的: 整数部分除二取余、从下往上读;小数部分乘二取整、从上往下读。 这一处最容易记混。

四、0.1 为什么存不下

0.625 三步就停了。但不是所有小数都这么听话。 0.1 转二进制是 0.0001100110011…,1 和 0 循环下去,永远停不下来。 道理和十进制里的 1/3 一样:1/3 写成小数是 0.3333…,也永远写不完。 只是换了个进制,写不完的数换了一批。

电脑的位数有限,只能截断。所以存进去的 0.1 并不是精确的 0.1,而是「最接近它的那个能存的数」。 这就是 0.1 加 0.2 在某些语言里不等于 0.3 的根源。它跟算法无关,跟「表示」有关。 工程上的处理办法是:比大小别用等号,用「差的绝对值小于某个很小的数」;金额别用小数,用整数分。

五、动手 Demo:173 除二取余,再按位权加回去

先把 173 一路除以 2,把每行的商和余数打出来,再从下往上读。 再用权重 128 到 1 逐位验证,看加出来是不是 173。 最后转一个带小数的 0.625,看三个 0 和 1 是怎么取的。

n, bits = 173, []
while n:
    print(n, n // 2, n % 2)
    bits.append(str(n % 2))
    n //= 2
print('从下往上读 0b' + ''.join(reversed(bits)))

s = '10101101'
total, w = 0, 128
for c in s:
    if c == '1':
        total += w
    print(c, w, total)
    w //= 2

f, out = 0.625, []
while f and len(out) < 8:
    f *= 2
    i = int(f)
    out.append(str(i))
    f -= i
    print('%.4f' % f, i)
print('小数部分 0.' + ''.join(out))

实跑输出:

--------------------------------------------------------------------
二进制与十进制互转
--------------------------------------------------------------------


  ① 十进制转二进制:除二取余
  --------------------------------------------------------------------
  把 173 一直除以 2,记下每次的余数:

| 当前数 | 除以 2 后的商 | 余数 |
|---:|---:|---:|
| 173 | 86 | 1 |
| 86 | 43 | 0 |
| 43 | 21 | 1 |
| 21 | 10 | 1 |
| 10 | 5 | 0 |
| 5 | 2 | 1 |
| 2 | 1 | 0 |
| 1 | 0 | 1 |

  余数从下往上读:0b10101101
  为什么从下往上读?因为先除出来的是最低位(个位对应的那一位),最后除出来的是最高位。
  顺序反了,数就全错了。

  ② 反过来:按位权展开
  --------------------------------------------------------------------
  10101101 一共 8 位,从左边开始权重是:
    128  64  32  16  8  4  2  1
  只把 1 的那几位加起来:128 + 32 + 8 + 4 + 1
  = 173

| 位 | 权重 | 这一位是 1 吗 | 加起来 |
|---:|---|---:|---|
| 1 | 128 | 1 | 128 |
| 2 | 64 | 0 | 不加 |
| 3 | 32 | 1 | 32 |
| 4 | 16 | 0 | 不加 |
| 5 | 8 | 1 | 8 |
| 6 | 4 | 1 | 4 |
| 7 | 2 | 0 | 不加 |
| 8 | 1 | 1 | 1 |

  每一位的权重是 2 的幂:1、2、4、8、16、32、64、128。记住这八个数,八位二进制就能心算了。

  ③ 小数部分:乘二取整
  --------------------------------------------------------------------
  把 0.625 转成二进制:

| 当前值 | 乘 2 | 取整数部分 |
|---:|---:|---|
| 0.6250 | 1.2500 | 1 |
| 0.2500 | 0.5000 | 0 |
| 0.5000 | 1.0000 | 1 |

  整数部分从上往下读:0.101
  验算:0.5 + 0.125 = 0.625
  整数部分「除二取余、从下往上读」,小数部分「乘二取整、从上往下读」。
  方向相反,这是最容易记混的一处。
  还有一件事要知道:0.1 这种十进制小数,转成二进制是无限循环的,存不下,只能截断。
  这就是 0.1 + 0.2 在某些语言里不等于 0.3 的根源。

①n // 2 是商,n % 2 是余数。这一对就是从 2.1 讲到现在的「能分几组、剩几个」。 ②bits 里存的是「先出来的低位」,所以最后要用 reversed 倒过来读。倒这一步忘了,结果就反了。 ③第二段从 128 开始,每轮 w //= 2 折半。打印出来的 total 是一路累加的过程。 ④小数那段 while f 的循环条件是「还有小数没取完」。 len(out) < 8 是保险——防 0.1 那种无限循环把程序卡死。

30 秒实验

  1. 把 173 换成 172,看除二取余的结果变成什么(提示:末位会变成 0)。
  2. 把 0.625 换成 0.1,看那个保险条件起没起作用。
  3. 写一句 0.1 + 0.2 == 0.3,看它返回什么。

常见误区

  1. 「余数从上往下读就行。」要从下往上读。反了不报错,只是数全错。
  2. 「小数也是除二取余。」小数是乘二取整、从上往下读,方向相反。
  3. 「0.1 存不存在是语言的问题。」二进制存不下 0.1,所有语言都一样,只是显示时四舍五入了。
自测三问(点开对答案) 1. 十进制转二进制怎么算? —— 一直除以 2 记余数,除到商为 0,余数从下往上读。 2. 八位二进制的权重是哪八个数? —— 128、64、32、16、8、4、2、1,从高位到低位依次减半。 3. 为什么 0.1 加 0.2 可能不等于 0.3? —— 0.1 转二进制是无限循环的,只能截断存储,存进去的已经不是精确的 0.1。

正数会转了,负数呢? 二进制里没有「减号」这个东西——下一节讲补码,一台只会加法的机器怎么算减法。

相关笔记