十进制与二进制转换算法背后的简单数学

如果你在网上搜索 How to convert from decimal to binary,会找到四个简单的算法:两个用于整数,两个用于小数。它们连同示例都放在本文第一部分。不过,虽然只记住算法在绝大多数情况下已经够用,我还是决定试着弄懂它们为什么成立。第二部分讲解每个算法背后那些非常基础的数学。懂了这些,万一你哪天突然忘了某个算法,也能把它推回来。我强烈建议你拿出纸笔,跟着我一起做一遍运算,这样能更好地记住其中的数学。下面就是网上能找到的那四个算法及示例。

把十进制整数转成二进制

要把整数转成二进制,从这个整数开始,把它除以 2,记下商和余数。继续用 2 去除商,直到商为零。然后把各次的余数按逆序写出来即可。

下面用整数 12 举例。先把这个数除以二,写出商和余数:

12:2=6+06:2=3+03:2=1+11:2=0+1\begin{aligned} 12 : 2 &= 6 + 0 \\ 6 : 2 &= 3 + 0 \\ 3 : 2 &= 1 + 1 \\ 1 : 2 &= 0 + 1 \end{aligned}

现在只需把余数按逆序写出来——1100。所以十进制的 12 在二进制中表示为 1100

把十进制小数转成二进制

要把小数转成二进制,从这个小数开始,把它乘以 2,记下所得的整数部分和小数部分。继续乘以 2,直到所得的小数部分等于零。然后把每次乘法结果中的整数部分写出来即可。

下面用小数 0.375 举例。

0.3752=0+0.750.752=1+0.50.52=1+0\begin{aligned} 0.375 \cdot 2 &= 0 + 0.75 \\ 0.75 \cdot 2 &= 1 + 0.5 \\ 0.5 \cdot 2 &= 1 + 0 \end{aligned}

现在只需把每一步所得的整数部分写出来——0.011。所以十进制的 0.375 在二进制中表示为 0.011

把二进制整数转成十进制

要把二进制整数转成十进制,从左边开始。取当前的累计值,乘以二,再加上当前这一位数字。继续下去,直到没有数字剩下。下面用整数 1011 举例。

20+1=121+0=222+1=525+1=11\begin{aligned} 2 \cdot 0 + 1 &= 1 \\ 2 \cdot 1 + 0 &= 2 \\ 2 \cdot 2 + 1 &= 5 \\ 2 \cdot 5 + 1 &= 11 \end{aligned}

把二进制小数转成十进制

要把二进制小数转成十进制,从右边开始,累计值取 0。取当前的累计值,加上当前这一位数字,再把结果除以 2。继续下去,直到没有数字剩下。下面用小数 0.1011 举例。我把除以 2 直接换成了乘以 1/2

12(1+0)=0.512(1+0.5)=0.7512(0+0.75)=0.37512(1+0.375)=0.6875\begin{aligned} \frac{1}{2} \cdot (1 + 0) &= 0.5 \\ \frac{1}{2} \cdot (1 + 0.5) &= 0.75 \\ \frac{1}{2} \cdot (0 + 0.75) &= 0.375 \\ \frac{1}{2} \cdot (1 + 0.375) &= 0.6875 \end{aligned}

这就是能让你在二进制和十进制之间来回转换的 4 个简单算法。

数的 q 进制展开

理解这些算法为何成立的关键,是数的 q 进制展开。任意进制中的整数都可以写成如下形式:

N=xnqn++x1q1+x0q0N = x_n \cdot q^n + \ldots + x_1 \cdot q^1 + x_0 \cdot q^0

其中 N 是整数,x 是数位 (十进制中为 0 到 9,二进制中为 0 和 1)q 是基数 (十进制为 10,二进制为 2)

本文中这种形式称为 数 N 的 q 进制展开,或简称 q 进制展开。我们看看数 12 在十进制和二进制中的样子:

1210=1101+210011002=123+122+021+020\begin{aligned} 12_{10} &= 1 \cdot 10^1 + 2 \cdot 10^0 \\ 1100_2 &= 1 \cdot 2^3 + 1 \cdot 2^2 + 0 \cdot 2^1 + 0 \cdot 2^0 \end{aligned}

同样地,任意进制中的小数也可以写成如下形式:

N=x1q1+x2q2++xnqnN = x_1 \cdot q^{-1} + x_2 \cdot q^{-2} + \ldots + x_n \cdot q^n

其中 N 是小数,x 是数位 (十进制中为 0 到 9,二进制中为 0 和 1)q 是基数 (十进制为 10,二进制为 2)

0.375 在十进制和二进制中的表示如下:

0.37510=3101+7102+51030.0112=021+122+123\begin{aligned} 0.375_{10} &= 3 \cdot 10^{-1} + 7 \cdot 10^{-2} + 5 \cdot 10^{-3} \\ 0.011_2 &= 0 \cdot 2^{-1} + 1 \cdot 2^{-2} + 1 \cdot 2^{-3} \end{aligned}

把十进制整数转成二进制

事实证明,我们可以用这个 q 进制展开形式 把一个数从十进制转成二进制。就用同一个数 12 来做。首先假装我们不知道它在二进制中怎么写,把未知的数位用 x 代替写出来:

1210=xn2n++x121+x02012_{10} = x_n \cdot 2^n + \ldots + x_1 \cdot 2^1 + x_0 \cdot 2^0

我们的任务是求出所有的 x。看看这里能做什么。首先要注意的是:除最后一项外,所有加数都会是偶数,因为它们都是 2 的倍数。利用这一点,我们就能推出 x0 这一位——如果被转换的整数是偶数,那么 x0 等于 0;如果是奇数,那么 x0 必然是 1。这里的数 12 是偶数,所以 x0 为零。把这个信息记下来:

1210=xn2n++x121+012_{10} = x_n \cdot 2^n + \ldots + x_1 \cdot 2^1 + 0

接下来要求 x1 的值。既然从 x1xN 的所有加数都是 2 的倍数,我们就可以把 2 提到括号外,从而把 x1 单独分出来。来做一下:

1210=2(xn2n1++x1206)+012_{10} = 2(\underbrace{x_n \cdot 2^{n-1} + \ldots + x_1 \cdot 2^0}_{6}) + 0

也很容易看出括号内各值之和等于 6。于是第一步可以写成:

12=26+012 = 2 \cdot 6 + 0

继续求剩下的 x。括号里的多项式可以单独写成一个式子:

610=xn2n1++x221+x1206_{10} = x_n \cdot 2^{n-1} + \ldots + x_2 \cdot 2^1 + x_1 \cdot 2^0

在这里,套用上面同样的逻辑就能看出 x1 等于 0。把它重写一遍,并再次把 2 提到括号外:

610=2(xn2n2++x2203)+06_{10} = 2 \cdot (\underbrace{x_n \cdot 2^{n-2} + \ldots + x_2 \cdot 2^0}_{3}) + 0

于是第二步是:

6=23+06 = 2 \cdot 3 + 0

现在能看出规律了。我们可以一直把 2 提出来,直到商为零。顺着这个规律走下去,看看会得到什么。

310=2(xn2n3++x3201)+13_{10} = 2 \cdot (\underbrace{x_n \cdot 2^{n-3} + \ldots + x_3 \cdot 2^0}_{1}) + 1

既然商等于 1,就只剩下一个加数了,于是把前面的式子重写一下:

310=2(x3201)+13_{10} = 2 \cdot (\underbrace{x_3 \cdot 2^0}_{1}) + 1

于是第三步是:

3=21+13 = 2 \cdot 1 + 1

最后我们得到:

110=x3201_{10} = x_3 \cdot 2^0

显然 x3 等于 1。但由于我们的算法需要一个商,把前面的式子重写成含有商的形式:

110=20+11_{10} = 2 \cdot 0 + 1

既然得到的商是 0,就再没有可处理的东西了,这就是最后一步。写出来:

1=20+11 = 2 \cdot 0 + 1

到这里转换就完成了。整个过程按步骤看是这样:

12=26+06=23+03=21+11=20+1\begin{aligned} 12 &= 2 \cdot 6 + 0 \\ 6 &= 2 \cdot 3 + 0 \\ 3 &= 2 \cdot 1 + 1 \\ 1 &= 2 \cdot 0 + 1 \end{aligned}

现在很清楚了:每一步的余数对应相应位置上 x 的值——第一个余数对应第一个 x,第二个余数对应第二个 x,以此类推。所以按上面描述的算法,数 12 在二进制中表示为 1100

别忘了我们一开始是想说明,那个「除以 2」的算法为什么成立。把上面这些步骤中的 2 移到式子左边:

12=26+012:2=6+06=23+06:2=3+03=21+13:2=1+11=20+11:2=0+1\begin{aligned} 12 = 2 \cdot 6 + 0 &\rightarrow 12 : 2 = 6 + 0 \\ 6 = 2 \cdot 3 + 0 &\rightarrow 6 : 2 = 3 + 0 \\ 3 = 2 \cdot 1 + 1 &\rightarrow 3 : 2 = 1 + 1 \\ 1 = 2 \cdot 0 + 1 &\rightarrow 1 : 2 = 0 + 1 \end{aligned}

这样你就能看到,我们是如何得到开头那个算法的。这四步的计算也可以合并成一个表达式,像这样:

12=2(2(2(20+11)+1203)+0206)+02012 = 2 \cdot (\underbrace{2 \cdot (\overbrace{2 \cdot (\underbrace{2 \cdot 0 + 1}_{1}) + 1 \cdot 2^0}^{3}) + 0 \cdot 2^0}_{6}) + 0 \cdot 2^0

请务必弄清我们是怎么得到这个表达式的——在探究二进制转十进制的算法如何成立时,我们会用到它。

把十进制小数转成二进制

为了说明把小数转成二进制时为什么要乘以 2 并取整数部分,我同样会使用小数的 q 进制展开形式。我用本文第一部分中的小数 0.375。和整数部分一样,先假装我们不知道这个数在二进制中怎么写,把未知的数位用 x 代替写出来:

0.37510=x121+x222++xn2n0.375_{10} = x_1 \cdot 2^{-1} + x_2 \cdot 2^{-2} + \ldots + x_n \cdot 2^n

和整数一样,我们的任务是通过逐个分离出 x 来求出所有的 x。看看该怎么做。首先要注意的是:2 的负次幂给出的是分母为 2 的正次幂的分数。把上面的式子重写一下:

0.37510=x112+x2122++xn12n0.375_{10} = x_1 \cdot \frac{1}{2} + x_2 \cdot \frac{1}{2^2} + \ldots + x_n \cdot \frac{1}{2^n}

一眼就能看出,式子右边可以直接把 1/2 提出来。来做一下:

0.37510=12(x1+x212++xn12n1)0.375_{10} = \frac{1}{2} \cdot (x_1 + x_2 \cdot \frac{1}{2} + \ldots + x_n \cdot \frac{1}{2^{n-1}})

然后把 1/2 移到左边

0.3752=x1+x212++xn12n10.375 \cdot 2 = x_1 + x_2 \cdot \frac{1}{2} + \ldots + x_n \cdot \frac{1}{2^{n-1}}

好,这样我们就把 x1 分离出来了,而且知道它只能是 10。要确定它是哪一位,先看看剩下的那些加数:

x212++xn12n1x_2 \cdot \frac{1}{2} + \ldots + x_n \cdot \frac{1}{2^{n-1}}

想一想这些数之和最大能有多大。如果 x 各位的最大值是 1,那就可以直接把 x 全换成 1,把这个和写成:

12+122+123+124++12n\frac{1}{2} + \frac{1}{2^2} + \frac{1}{2^3} + \frac{1}{2^4} + \ldots + \frac{1}{2^n}

这是一个分数构成的等比数列,这类数列的和落在 [0 < 和 < 1] 的范围内,所以这个和最大能给出的值就是 1。现在再看我们的式子:

0.3752=x1+x212++xn12n1最多为 10.375 \cdot 2 = x_1 + \underbrace{x_2 \cdot \frac{1}{2} + \ldots + x_n \cdot \frac{1}{2^{n-1}}}_{\text{最多为 } 1}

现在应该清楚了:如果右边小于 1,那么 x1 就不可能等于 1,因此它等于 0,而剩下的部分等于 0.75

0.3752=0+x212++xn12n10.750.375 \cdot 2 = 0 + \underbrace{x_2 \cdot \frac{1}{2} + \ldots + x_n \cdot \frac{1}{2^{n-1}}}_{0.75}

这与开头给出的算法第一步完全一致:

0.3752=0+0.750.375 \cdot 2 = 0 + 0.75

取出 0.75 的小数部分,再提出一个 1/2,把 x2 分离出来:

0.75=12(x2++xn12n2)0.75 = \frac{1}{2} \cdot (x_2 + \ldots + x_n \cdot \frac{1}{2^{n-2}})

并把 1/2 移到左边:

0.7521.5=x2+x312++xn12n2\underbrace{0.75 \cdot 2}_{1.5} = x_2 + x_3 \cdot \frac{1}{2} + \ldots + x_n \cdot \frac{1}{2^{n-2}}

此时,如果 x2 等于 0,那么式子右边之和不可能大于 1,但左边是 1.5,所以 x2 必须是 1,剩下的部分是 0.5。写出来:

0.7521.5=1+x312++xn12n20.5\underbrace{0.75 \cdot 2}_{1.5} = 1 + \underbrace{x_3 \cdot \frac{1}{2} + \ldots + x_n \cdot \frac{1}{2^{n-2}}}_{0.5}

这同样符合开头给出的算法的规律:

0.752=1+0.50.75 \cdot 2 = 1 + 0.5

对剩下的小数部分 0.5 重复同样的操作。

0.521=x3++xn12n3\underbrace{0.5 \cdot 2}_{1} = x_3 + \ldots + x_n \cdot \frac{1}{2^{n-3}}

用上面同样的逻辑可以看出 x3 等于 1,并且没有剩余的小数部分:

0.521=1++xn12n30\underbrace{0.5 \cdot 2}_{1} = 1 + \underbrace{\ldots + x_n \cdot \frac{1}{2^{n-3}}}_{0}

既然剩余的小数部分等于 0,我们最后一步就是这样:

0.52=1+00.5 \cdot 2 = 1 + 0

把所有步骤再写一遍:

0.3752=0+0.750.752=1+0.50.52=1+0\begin{aligned} 0.375 \cdot 2 &= 0 + 0.75 \\ 0.75 \cdot 2 &= 1 + 0.5 \\ 0.5 \cdot 2 &= 1 + 0 \end{aligned}

这正是我在开头给出的算法。和处理整数时一样,这三步的计算也可以合并成一个表达式:

0.375=12(0+12(1+12(1+0)))0.375 = \frac{1}{2} \cdot (0 + \frac{1}{2} \cdot (1 + \frac{1}{2} \cdot (1 + 0)))

同样,请务必完全掌握这个表达式——在探讨二进制转十进制时我们会用到它。

为什么并非所有小数都能在二进制中有限表示

有些在十进制中能有限表示的小数,在二进制中却无法有限表示——这一点让很多开发者感到意外。但恰恰是这个困惑,是 0.1 加 0.2 会得出看似古怪结果的根源。那么,是什么决定了一个小数能否在某个进制中被有限表示呢?要让一个数能有限表示,分数的分母必须是该进制基数的幂。例如在十进制中,分母必须是 10 的幂——这就是为什么 0.625 在十进制中可以有限表示:

625103=0.625\frac{625}{10^3} = 0.625

而 1/3 无法有限表示:

13=0.3333=0.3\frac{1}{3} = 0.3333\ldots = 0.\overline{3}

二进制也是同理:

6251000=58=523\frac{625}{1000} = \frac{5}{8} = \frac{5}{2^3}

但如果看 0.1,它的分母是 10,而 10 不是 2 的幂,所以 0.1 在二进制中会是一个无限小数。用上面学到的算法验证一下:

0.12=0+0.20.22=0+0.40.42=0+0.80.82=1+0.60.62=1+0.20.22=0+0.40.42=0+0.8\begin{aligned} 0.1 \cdot 2 &= 0 + 0.2 \\ 0.2 \cdot 2 &= 0 + 0.4 \\ 0.4 \cdot 2 &= 0 + 0.8 \\ 0.8 \cdot 2 &= 1 + 0.6 \\ 0.6 \cdot 2 &= 1 + 0.2 \\ 0.2 \cdot 2 &= 0 + 0.4 \\ 0.4 \cdot 2 &= 0 + 0.8 \\ &\ldots \end{aligned}

这样可以无限进行下去,不过我们把它写成循环小数:

0.110=0.000110011001100112=0.000110.1_{10} = 0.00011001100110011\ldots_2 = 0.0\overline{0011}

把二进制整数转成十进制

我用第一节中同一个二进制整数 1011,来说明那个「乘以 2」的算法为什么成立。这里同样要用到数的 q 进制展开形式。先把它写成这种形式:

10112=123+022+121+1201011_2 = 1 \cdot 2^3 + 0 \cdot 2^2 + 1 \cdot 2^1 + 1 \cdot 2^0

既然所有加数都是 2 的倍数,我们就可以不断把 2 提出来,直到商为零。来做一下:

10112=2(2(2(20+1)+0)+1)+11011_2 = 2 \cdot (2 \cdot (2 \cdot (2 \cdot 0 + 1) + 0) + 1) + 1

现在,只要按照数学运算的顺序算下去,你得到的就正是我在开头展示的那些步骤,具体来说:

1110=2(2(2(20+11)+02)+15)+111_{10} = 2 \cdot (\underbrace{2 \cdot (\overbrace{2 \cdot (\underbrace{2 \cdot 0 + 1}_{1}) + 0}^{2}) + 1}_{5}) + 1 20+1=121+0=222+1=525+1=11\begin{aligned} 2 \cdot 0 + 1 &= 1 \\ 2 \cdot 1 + 0 &= 2 \\ 2 \cdot 2 + 1 &= 5 \\ 2 \cdot 5 + 1 &= 11 \end{aligned}

这样,二进制的 1011 就是十进制的 11

把二进制小数转成十进制

现在我们来到了最后一个算法。也许你已经自己想明白它的机理了。如果还没有,我们就看看它为什么成立。数的 q 进制展开形式 在这里同样是关键。我们取第一节中的数 0.1011,把它写成展开形式:

0.10112=112+0122+1123+11240.1011_2 = 1 \cdot \frac{1}{2} + 0 \cdot \frac{1}{2^2} + 1 \cdot \frac{1}{2^3} + 1 \cdot \frac{1}{2^4}

同样,既然所有加数都是 1/2 的倍数,我们就可以不断把 1/2 提出来,直到没有剩余的小数部分。来做一下:

0.10112=12(1+12(0+12(1+12(1+0))))0.1011_2 = \frac{1}{2} \cdot (1 + \frac{1}{2} \cdot (0 + \frac{1}{2} \cdot (1 + \frac{1}{2} \cdot (1 + 0))))

按照数学运算顺序算下去,就得出开头所述的算法:

0.687510=12(1+12(0+12(1+12(1+0)0.5))0.75)0.3750.6875_{10} = \underbrace{\frac{1}{2} \cdot (1 + \overbrace{\frac{1}{2} \cdot (0 + \frac{1}{2} \cdot (1 + \underbrace{\frac{1}{2} \cdot (1 + 0)}_{0.5}))}^{0.75})}_{0.375} 12(1+0)=0.512(1+0.5)=0.7512(0+0.75)=0.37512(1+0.375)=0.6875\begin{aligned} \frac{1}{2} \cdot (1 + 0) &= 0.5 \\ \frac{1}{2} \cdot (1 + 0.5) &= 0.75 \\ \frac{1}{2} \cdot (0 + 0.75) &= 0.375 \\ \frac{1}{2} \cdot (1 + 0.375) &= 0.6875 \end{aligned}

这样,二进制的 0.1011 就是十进制的 0.6875