0. 从一个BUG说起
(注:没有编程经验或者只想从二进制看起的朋友,可以跳过这一节,直接看1. 认识二进制章节。)
若干年前,在我开发的一个项目中,出现了一个奇怪的问题,用一段代码抽象出来大概是这个样子:
i = 0
while i < 2:
if i == 0.9:
print(0.9)
elif i == 1.2:
print(1.2)
elif i == 1.5:
print(1.5)
i += 0.1
猜这段代码最终会输出什么?嗯,1.2
,这段代码只输出了1.2
。也就是说,在这段代码中,i == 0.9
和i == 1.5
这两个判断都是false
,因此没有执行接下来各自块内的print
代码。
这就很令人费解了,因为这段代码的逻辑非常简单,即便是刚学编程的新人,也能看出来,这段代码在逻辑上没有任何问题:i
从0
开始,每次+0.1
,那么i
必然会有过i == 0.9
和i == 1.5
这两个值。这个逻辑清晰,数学上也正确,并且代码输出了1.2
,说明代码一定也走过了0.9
,但为什么在我们的代码里,0.9
和1.5
都没有输出呢?
实际上,如果我们把代码改一下,让它每次循环时,都输出当前的值,我们就能发现一些端倪:
# 代码
i = 0
while i < 2:
print(i)
i += 0.1
# 结果
0
0.1
0.2
0.30000000000000004
0.4
0.5
0.6
0.7
0.7999999999999999
0.8999999999999999
0.9999999999999999
1.0999999999999999
1.2
1.3
1.4000000000000001
1.5000000000000002
1.6000000000000003
1.7000000000000004
1.8000000000000005
1.9000000000000006
看到了吗?很奇怪对不对?这是我在Python 3.6
下的输出结果,有兴趣的朋友可以试试其他语言,看看是不是这样的结果。
这样的结果就自然而然地引发了我们的疑问:
为什么是这样?
这一切,都和我们这篇系列文章所谈论的主题————二进制有关。
0.1. 0.30000000000000004
其实上面的这个问题,在整个计算机界都是非常出名的一个问题,它有一个更为人熟知的描述:0.1 + 0.2 = 0.30000000000000004
,甚至还有一个专门的网站Floating Point Math统计了各个语言中0.1 + 0.2
的结果。而这个问题的根源,在于计算机中浮点数的精度。1989年,计算机科学家William Kahan凭借浮点运算的数值分析研究拿到了图灵奖,同时,William Kahan也是浮点运算标准IEEE 754
和IEEE 854
的主要设计师。
那么这个所谓的精度问题到底是怎么回事呢?由于这一节是整篇文章抛砖引玉的一节,这篇文章的重点在二进制本身,因此这里先用几句话简单地解释一下这个精度问题。简单说,就是:
计算机中所有的运算、存储,都是以二进制进行的,二进制可以精确地表示整数,但却无法精确表示一些分数(小数)。所以,我们看到的0.1
在计算机中实际存储的并不是那个我们熟知的1/10
,它只是一个与0.1
非常接近的近似值,而这个值,可能是0.10000000000000001
,也可能是0.1000000000000000055511151231257827021181583404541015625
,但它终究只是一个近似值。同理,0.2
,0.3
等小数也是近似值,因而在文章一开始的代码中,我们用于判断的0.9
,和由程序循环计算出来的0.9
并不是同一个0.9
,它们在我们看不到的位数上有着精度的差异,就像上面的两个0.1
的近似值,而这种差异在程序做数值比较时,被判定为了false
,不相等,于是有了文章开头看到的程序“BUG”。
那么为什么二进制无法精确表示某些小数呢?这个就是二进制的具体计算方式导致的了,我们留到后面再讲。
解释完了精度问题的大概原因,我们再回过头来看看文章开头的那段代码,既然这个BUG的源头是计算机的问题,那么我们在代码中有办法解决么?其实是有的。
许多语言都提供了高精度运算的工具,用这些高精度运算方法,就可以解决上面代码中的问题了。依然以Python
为例,在Python
的标准库中,有一个叫做decimal
的库,用它就可以完美解决我们的问题,修改后的代码如下:
from decimal import Decimal
i = Decimal('0')
while i < Decimal('2'):
if i == Decimal('0.9'):
print(0.9)
elif i == Decimal('1.2'):
print(1.2)
elif i == Decimal('1.5'):
print(1.5)
i += Decimal('0.1')
这样之后,0.9
、1.2
、1.5
就都可以按照代码逻辑打印在屏幕上了。
如果我们的语言没有这种方便的高精度库呢?这里再提供一种方法:
epsilon = 0.01
i = 0
while i < 2:
if abs(i - 0.9) < epsilon:
print(0.9)
elif abs(i - 1.2) < epsilon:
print(1.2)
elif abs(i - 1.5) < epsilon:
print(1.5)
i += 0.1
这种方法同样可以修复这段代码的BUG
。对高等数学熟悉的朋友可能注意到了,这种方法其实就是借鉴了高数中极限的定义。放在这段代码中就是说,即使这个i
并不能真正等于0.9
,但它在计算机中总是有一个精度,也就是在这个精度范围内无限接近0.9
,那么我们设置一个epsilon
(也就是数学中常见的希腊字母ε),令epsilon
足够小,至少比i
的变化值要小,但又大于存储的精度,用它来判断i
是否趋近于0.9
,如果趋近了,我们就认为i
此时已经可以看作是0.9
。
1. 认识二进制
简单解决了上文中的问题后,我们来看看今天的主角,二进制。
1.1 进位计数制
平时我们所说的二进制、八进制、十进制以及十六进制等等各种进制,其实本质上是一种计数方式。即所谓进制
,就是进位计数制
,不同进制之间的区别,也仅仅是进位的方式的区别。
人类在日常生活中,多使用十进制,即由0
到9
十个数字组成的数,计数时逢十进一。举个计数的例子,我们从00
开始(补充十位上的0
),接着是01
,02
,一直到09
,然后下一个数字是第十一个数字,超出了十进制的十
的限制,因此我们在第二位(从右数,即十位)上进一,变成1
,个位回归0
,最终组成了10
。据说,人类之所以选择了十进制,是因为人类有十根手指,在做计算时非常方便。那人类文明进化的过程中,有使用其他进制嘛?有的,包括二进制、五进制甚至二十进制都曾在历史上出现过。比如我们中国在唱票时常见的正
字计数法,就是一种五进制。
我们知道,计算机内部采用的是二进制,即0
和1
组成的数字,逢二进一。那么计算机为什么会采用二进制?道理和上面人类的十进制类似,因为二进制最符合电子元器件的二元逻辑,即高电平和低电平,或者理解为开(通)和关(不通)。二进制在计数上和十进制一样,最右边为最低位,越往左位数越高,唯一不同的是“逢二进一”。举个例子,我们从0
开始,00
,然后是01
,到这里为止,二进制和十进制表示的数字一样,就是0
和1
。但此时已经计了两个数了,1
的下一个数应该是2
,而二进制里只有0
和1
,于是01
的下一个数,第二位(从右往左)进一,变成1
,第一位回归0
,组合后为10
。
看到了吗,正如前文所述,二进制和十进制的计数非常相似,除了进位不同,整个计数过程完全一样。除了它们俩之外,另外两个我们经常遇到的进制,八进制和十六进制,在计数这个基础功能上,过程也和他们一模一样。甚至如果我们随意设计一个进制,譬如三进制,四进制,他们的计数方式也依然是上文描述的方式。
上面两个进制的例子中,另外一个我们要注意的是,无论是二进制还是十进制,它们在第一次进位后,都变成了10
,但这个10
在两个进制中表示的数字却并不相同。十进制中的10
就是我们平时所知道的10
,而二进制中的10
则表示的是十进制中的2
,或者说,是我们在计数时从0
开始后的第三个数。根据这个规律,我们可以隐约猜到,如果我们用八进制和十六进制,它们各自的10
,也就是第一次进位的数字,应该分别代表十进制中的8
和16
。
我们似乎窥见了一丝进制转换,尤其是其他进制转换成十进制的方法和规律,那么这种转换具体是怎么进行的呢?我们继续往下看。
2. 进制的转换(整数)
2.1 十进制分解
由于我们平时都用十进制,而且阿拉伯数字的设计本身也符合十进制的特点,所以我们在长年累月的潜移默化下,整个数字思维也变成了十进制思维,除非经过特殊训练,否则普通人在对数字进行相关操作时,大脑总是以一种十进制的思想去执行。可以这么说,我们的大脑被我们训练成了一台十进制的计算机。
也正因为如此,将其他进制的数字转换成十进制,是一种相对容易,且符合我们思维习惯的一种做法————将一种我们不熟悉的东西,转化成我们熟悉的东西。
在讲进制转换之前,我们先来审视一下我们最熟悉的十进制。
我们随便给一个十进制整数,譬如123
好了,这个数字,我们在中文中读作一百二十三,也就是说,123
这个数字,是由100
、20
、3
这三个数字相加结合而成的。这非常地显而易见,因为百位上是1
,也就是1 x 100
,十位上是2
,即2 x 10
,个位上是3
,3 x 1
,最终1 x 100 + 2 x 10 + 3 x 1 = 123
。尽管我们平时不会真的这么去算123
,因为这个数字本身已经是一个能够深入脑海的十进制数了,但当我们把123
这个数字的组成方式拆开来看时,它的整个过程也不会让我们有任何的不适感,因为,个、十、百、千、万,我们日常见到的十进制数都是这么组成的,这种组合方式是非常自然而然的。
既然上面已经拆解了一个十进制数,那么我们可不可以更进一步,用一个数学公式去表示他?
依然是123
这个例子,我们观察一下拆解后的1 x 100 + 2 x 10 + 3 x 1 = 123
,我们发现,这是一个典型的多项式
,1
、2
、3
是系数,后面的100
、10
、1
则是10
的指数,即\(10^n\),于是,上面的式子就变成了:
$$1\times10^2+2\times10^1+3\times10^0=123$$
更一般地,我们将多项式的系数用\(a_n\)代替,将最终结果123
用大写字母S
表示,然后把上式中的顺序稍微变一下,便得到了这么一个式子:$$S=a_1\times10^0+a_2\times10^1+a_3\times10^2$$
这就是一个标准的十进制数字的组成了,于是我们开始思考:这个多项式每项的系数是我们看到的数字,这个很好理解,但后面的指数部分的底数,为什么是10
?许多朋友肯定会说了:因为是十进制啊!
没错,因为是十进制。我们再剖析一下123
这个数字,第一位(个位)上是3
,而个位并没有进位,它们全都小于10
,因此个位上的指数是\(10^0\)。然后是第二位,十位,十位上是2
,但十位上的所有数字都是个位进位后得到的,所以当十位上出现非0
的数字时,整个数字最小就是10
,再进位变成20
,一直到十位上变成9
,十位就到头了,再变就得进位到第三位(百位)上了,于是,第二位的指数部分自然而然地就是\(10^1\),按照刚才的推论,第三位的指数部分就是\(10^2\)。
对编程敏感的朋友可能注意到了,上面的这个计数 + 进位的过程,是不是很像一个多重循环?
# 可以输出0到999所有1000以下的数
for third in range(10):
for second in range(10):
for first in range(10):
print(int('{0}{1}{2}'.format(third, second, first)))
2.2 其他进制转换成十进制
重点来了,上文里我们说到,十进制数拆分后的多项式指数部分的底数之所以是10
,是因为我们用的是十进制,那么如果是二进制呢?这个底数是什么?会不会是……2
!我们来试一试。
首先,我们随便取一个二进制整数,譬如101
,第一位(右边开始)和第三位都是1
,中间是0
,我们先按照原始的计数法来数一下,这个数字是几(十进制)。
从000
开始,接着是001
,010
,011
,100
,101
,后面这四个数用十进制计数后,分别是:1
,2
,3
,4
,5
,所以,101
这个二进制数字实际上是5
。我们再按照前面所说的多项式来计算一下,看到底是不是5
。注意了,此时多项式的指数底数已不再是10
,而是本节一开头所猜测的2
,得到式子和结果如下:
$$1\times2^2+0\times2^1+1\times2^0=5$$
真的是5
!所以这个多项式的指数底数真的就是2
,我们的猜测似乎是正确的?自信点,把似乎拿掉,我们的猜测就是正确的,大家感兴趣的可以手动试试其他整数的计算。
那么这样一来,我们已经顺利地完成了二进制到十进制的转化……等一下,为什么我可以这么笃定地说这样的转化结果就是十进制了?是不是太草率了一点?为什么不是别的进制结果?
这是个好问题,我第一次接触二进制的转换计算时,也发出过这样的疑问。这个问题的答案其实也很简单:多项式还是那个多项式,计算的结果取决于我们计算时用的是什么进制。什么意思呢?也就是说,因为我们在计算这个多项式时使用的是十进制,所以我们得到的结果自然也是十进制的。换言之,如果上面的两个例子,我们使用其他进制进行计算,得到的数字结果会是其他进制的结果了。当然,在二进制101
这个例子里,出现的所有数字都小于等于5
,因此,最终得到的5
这个结果,同样也是八进制和十六进制的结果。
原理是这样没错,但实际上我们手动转换时却并不能这么操作,会有各种问题和陷阱,譬如十进制123
这个数,我们想用二进制去计算它的多项式,得到的式子是这样:
$$1\times1010^{10}+10\times1010^1+11\times1010^0=?$$
嗯,没错,这个二进制的式子我们自己不会算,只能借助编程语言或者其他工具来计算:
s = int('1', 2) * int('1010', 2) ** int('10', 2) + int('10', 2) * int('1010', 2) ** int('1', 2) + int('11', 2) * int('1010', 2) ** int('0', 2)
print(s, bin(s)[2:])
# 结果
123, 1111011
Python
帮我们计算出了这个数字,的确是123
,但它会默认以十进制的形式表示,因此这里我们用二进制的转换函数bin()
来转换一下,就得到了二进制的结果1111011
。
再譬如,我们把123
这个十进制数的多项式表达式用八进制计算一下:
$$1\times12^2+2\times12^1+3\times12^0=?$$
相比较上面二进制的式子,这个就顺眼多了,因为它长得很十进制
。当然了,仅仅是像十进制,它并不是十进制,只是八进制。此时,如果我们下意识地开始计算,就会再次掉入进制陷阱:我们在用十进制的计算方式计算八进制。
$$1\times12^2+2\times12^1+3\times12^0=171(×)$$
$$1\times12^2+2\times12^1+3\times12^0=173(√)$$
上面第一个式子是我们直接
去计算的结果,也就是用十进制来计算的八进制,结果显然是错误的;第二个式子则是用八进制的方式进行的计算,得到的结果173
正是123
的八进制表示。尽管171
这个结果是错的,但我们可以很明显地注意到,171
和正确值173
非常接近,然而这种接近仅仅是因为123
这个原数字并不大,而且十进制和八进制又相对接近,所以得到的结果误差很小。甚至有时候我们用错误的方式计算八进制,也会得到正确的答案,譬如十进制正整数12
,它的八进制数是14
,我们用十进制的方式来做一下八进制的计算:
$$1\times12^1+2\times12^0=14$$
结果确实是14
,似乎是正确的,但这仅仅是小整数下的巧合。
好了,我们已经可以将其他进制的数字(整数)正确转换成十进制了,我们把前面用到的这些式子再归纳一下,组成一个最终的数学公式:
$$\sum_{i=1}^{n}a_ix^{i-1}$$
上式中,n
为该数字的位数,\(a_i\)是每一位上的数,即多项式每项的系数,\(x^{i-1}\)则为指数部分,其中\(x\)为该数字的进制数,譬如二进制做转换时,\(x\)就是2
。根据这个公式计算出来的结果,就是其他进制的整数转换成十进制的结果了。
2.3 十进制转其他进制
本文从这里开始,如果没有什么特殊情形,将减少或不再讲解二进制和十进制以外的进制了,因为从前文我们可以发现,不同进制的计数、计算等操作都非常相似,就像本章开头所说,它们仅仅是进位的方式不同,因此大家后面可以非常容易地举一反三,通过二进制的一些计算和特性,来推算其他进制的情况。
回归正文。由于我们人类平时的计算方式和习惯都是基于十进制的,因此我们如果想要手动把任意进制转换成其他非十进制,除非精通其他进制的各种计算,否则就只能通过十进制中转了。那么十进制要如何正确转换成其他进制呢?
我们回看一下前文,其他进制的整数在转换成十进制时,主要做了乘法和加法的操作,假设十进制转其他进制是上面操作的逆操作,那么是否意味着,我们将一个整数从十进制转成其他进制,需要使用除法和减法?
没错,你猜对了。
不知道大家有没有注意到,前文中我们拆解十进制123
这个数字时,凭感觉直接将整个数字拆解成了1
、10
、100
和各自系数相乘累加的多项式,其实这一节我们要讨论的所谓逆运算,原理和这个拆解很像。
我们依然拿十进制数123
做文章,将它用十进制的另一种方式把各个位上的数字一个一个拆解出来,怎么拆呢?既然是十进制,那么我们让123
除以10
来看一下能得到什么结果:
$$123\div10=12......3$$
结果是12
余3
,这个余数3
,就是我们的个位上的数字。此时,123
没有了个位,变成了12
,也就是拆解个位时得到的结果。我们以此类推,再来把12
中的2
给拆解出来:
$$12\div10=1......2$$
这样我们又得到了十位上的2
,123
由这次拆解后,只剩下了百位的1
,我们接着拆解:
$$1\div10=0......1$$
只看这三个式子的余数,我们经过三次运算,依次得到了3
、2
、1
,由于第一次运算时的余数3
是个位的余数,因此它应该在个位上,我们以一种队列的形式(先进先出),把这三个数组合在一起,就变成了123
,也就是拆解之前的原数。相对于2.1章节中凑数式的暴力分解,这种拆解方式是不是就温和,也数学得多呢?
为什么能这么拆解?
其实原理显而易见:因为我们要得到十进制,因此用10
去除。换句话说,就是,这种拆解方式符合十进制的计数方式,即,逢十进一。逢十进一的意思是说,这个整数,它进了几次位,就必然经历过几个10
,数字中就有几个10
。
举个例子,13
,这个数字是由9
进位到10
,然后再计数到13
,它只进位了一次,还没有进行第二次进位,因此这个数中就只有一个10
。那么我们的123
这个数呢?它在计数的过程中,一共进位了12
次,最后的3
是在第十二次进位后的计数,所以当我们第一次除以10
时,就拿到了12
这个数字,拆出了3
这个余数。而进位的12
次中,又可以再细分成n
个10
,于是就再次做除法和减法(余数),最终拆到结果为0
,就意味着这个数字中已经没有10
了。
还记得章节 1.中提到的中国的正
字计数法吗?我们在唱票完统计结果时,通常会直接去数有几个正
,再把正
的数量乘5
,然后加上最后没有组成正
的字的笔画数。譬如我们数了21
个正,最后还剩了一个一
,那么这个计数结果就是:\(21\times5+1=106\)。
咦?这个式子,不就是上面我们拆分123
时第一个式子的逆运算吗?5
是除数,1
是余数,21
是结果。那按照123
的拆解法,我们是不是可以把这里的21
再用5
拆解呢?
$$21\div5=4......1$$
$$4\div5=0......4$$
我们再次得到了三个余数1
、1
、4
,所以我们把这三个数组成一个新的整数:411
,这个整数难道就是五进制中的106
吗?我们用上一章的方法验证一下:
$$4\times5^2+1\times5^1+1\times5^0=106$$
完全正确!
好了,我们果然又通过十进制的分解,找到了十进制转化到其他进制的算法和规律了,接下来就轮到二进制了。
给一个十进制正整数13
,我们用2
去除:
$$13\div2=6......1$$
$$6\div2=3......0$$
$$3\div2=1......1$$
$$1\div2=0......1$$
结果是1101
,验证一下:
$$1\times2^3+1\times2^2+0\times2^1+1\times2^0=13$$
果然是13。这样一来,十进制整数如何转换成其他进制的方法,我们也完全掌握了,大家有兴趣的可以去试试这里没提到的八进制、十六进制等等,正如前文中所提到的,它们的原理都一样,计算方式也仅仅是进位方式不同。
3. 小数的转换
这篇文章第一次截稿的时候并没有写这一章,因为前面内容实在有点多,打算把小数留到下一篇。后来思来想去,决定还是把小数的转换写在这篇的结尾,以和章节 0.相呼应。
3.1 转成十进制
在讲解其他进制转换成十进制之前,我们遵从前面的习惯,这里依然从解析十进制数自身开始,接着推出其他进制的转换法。
取一个十进制小数,0.123
好了,有了前面整数的详细推演过程,我们这里可以直接把0.123
拆解了。记得前面123
的第一次拆解么?123
是由1
个100
,2
个10
,3
个1
组成,对于十进制来说,分别是\(1\times10^2\)、\(2\times10^1\)、\(3\times10^0\)。我们注意到,在个位时,10
的幂已经从百位上的2
递减成了0
,那么我们可以猜测,如果再往右移,到了小数部分,指数n
再递减,就会变成-1
、-2
等等,也就是说,0.123
的小数部分,可以写成:
$$1\times10^{-1}+2\times10^{-2}+3\times10^{-3}=1\times0.1+2\times0.01+3\times0.001=0.123$$
由此推演出的十进制小数的计算,很符合我们的直觉。那么既然十进制小数的拆解是这个样子,那么按照前面我们整数的推演流程,如果我们要把一个二进制转换成十进制,是否只需要将上面式子中的10
都换成2
就好了呢?譬如这里有一个二进制小数0.101
,按照我们的推演,它如果要转换成十进制,应该是:
$$1\times2^{-1}+0\times2^{-2}+1\times2^{-3}=\frac{1}{2}+0+\frac{1}{8}=0.5+0.125=0.625$$
由于Python
中没有直接把二进制小数转十进制的内置函数,我们可以在网上找一个在线转换进制的工具,自己试一下,会发现上面我们推演的这个式子是成立的,也就是说,二进制数0.101
确实就是十进制数0.625
。
这就是二进制小数转十进制的方法了,那么如果一个数字既包含小数又包含整数部分呢,譬如11.01
?只要把整数部分和小数部分分别计算后结果再相加就行了:
$$\sum_{i=1}^{n}a_ix^{i-1}+\sum_{j=1}^{m}a_jx^{-j}$$
其实写到这里我们发现,我们在将二进制转化成十进制时,完全没有必要去太在意它的整数和小数的计算,我们只要累加这个组合:\(a_ix^{i-1}\),从小数点开始,小数点左边的位数i>=0
,右边的位数i<0
,从小数点往两边i
的绝对值越来越大,这样一来,直接将一个浮点数看成一个整体就可以了。
同样的,如果是其他进制转成十进制,方法也是如此,这里就不再展开了。
3.2 十进制转二进制
好了,我们终于来到了进制转换的最后一部分,十进制小数转成二进制。我们依然从十进制入手,用另一种分解方法来分解一个十进制小数,譬如……0.123
。
前面我们在第二次分解十进制整数时,用到了除法,即无限次除以10
,直到商为0
为止。小数部分,原理和整数部分类似,但过程稍微有些不同。我们先让0.123
除以0.1
,然后把商的整数和小数部分给分开:
$$0.123\div0.1=1.23=0.23+1$$
我们得到了0.23
和1
两部分,这里的1
就类似于前面整数分解时的余数,0.23
相当于商,于是我们继续分解这个0.23
,直到最终式子的小数部分变成0
:
$$0.23\div0.1=2.3=0.3+2$$
$$0.3\div0.1=3=0+3$$
扔掉三个式子的结果的小数部分,只取整数部分,再把它们依次排序,放到0.
的后面,就得到了0.123
这个结果。emmm……有点小问题,之前处理整数部分时,在最后组合的时候,这个组合顺序似乎是倒过来的?第一个解析出来的是个位(最右边),然后是十位、百位,都在个位的左边。而这里我们把第一个解析出来的1
放到了结果的最左边,然后往右组合。这种差异容易让我们在计算时产生错误,现在因为计算的是十进制,符合我们的直觉,我们可能会觉得没什么,但如果我们接下来计算二进制,这样的差异性极有可能给我们带来麻烦。
要解决这个问题,我们只需要更新一个概念即可。即,一个任意浮点数,譬如321.123
,我们抛弃前面说的那些所有的什么十位、百位,左边,右边第几位等等乱七八糟的概念,而将他们的位数简单分成高位
和低位
两部分,怎么分呢?数字的小数点并不是一个位数,我们以它为一个基准,将它看做位数最低点,以小数点开始,往两边位数逐渐升高,也就是使整体呈现一个V
字形。
有了这个概念后,我们再来看拆解和组合:无论是整数拆解还是刚刚进行的小数拆解,第一个拆出来的数字,放在属于它的最低位,第二个放在次低位,以此类推,直到整个数字组合完成。这样一来,他们组合的方式不会再让我们困扰,整个过程非常自然,从低位到高位。
然后我们再来看十进制的小数部分如何转成二进制。
上面十进制的拆分,我们除数部分用的是10
,也就是\(10^{-1}\),二进制应该使用\(2^{-1}\),但无论我们用\(2^{-1}\)还是0.5
,在写式子并计算的时候,都不是非常直观,所以这里简单做个调整,我们把\(\div2^{-1}\)换成\(\times2\)。整个计算过程的原理是一样的,我们也是把结果分成整数和小数两部分,然后取整数部分,从低位到高位组合起来。我们取上一节用过的一个十进制小数0.625
,然后开始计算:
$$0.625\times2=1.25=0.25+1$$
$$0.25\times2=0.5=0.5+0$$
$$0.5\times2=1=0+1$$
我们得到了最终结果:0.101
,这个结果就不用再验证了,我们在上一节刚刚用过这个数字。
3.3 小数导致的问题
了解了十进制小数如何转化成二进制后,我们就来看看本篇文章开头部分所提到的一个问题:二进制无法精确表示十进制的小数。
上面我们使用0.625
成功转化成了二进制的0.101
,但并不是所有小数都可以这么成功的转化,举一个非常简单的例子:0.1
(十进制),我们看看如果将它转换成二进制,会变成什么:
$$0.1\times2=0.2=0.2+0$$
$$0.2\times2=0.4=0.4+0$$
$$0.4\times2=0.8=0.8+0$$
$$0.8\times2=1.6=0.6+1$$
$$0.6\times2=1.2=0.2+1$$
$$(0.2再次出现,式子进入循环)$$
$$0.2\times2=0.4=0.4+0$$
$$0.4\times2=0.8=0.8+0$$
$$0.8\times2=1.6=0.6+1$$
$$0.6\times2=1.2=0.2+1$$
$$......$$
我们看到了,这个式子在解析的过程中,无法让小数部分清零,出现了无限循环。可以预见,照这么继续计算下去,0.1
的二进制的最终结果会是0.0001100110011...
永远没有尽头。这就是为什么二进制小数无法总是精确表示十进制小数的原因了,同样的,0.3
,0.9
这样的数,也无法真正转换成二进制。因为不精确,所以计算机会给一个近似值,而近似值在计算的过程中,误差逐渐超过比较时的精度,最终造成了0.1 + 0.2 != 0.3
的BUG
。
4. 结尾
这篇文章字数有点多,能看到这里的都是真的猛士。接下来可能还会再写个两到三篇关于二进制的系列文章(计划),后面会讲一下二进制的一些运算,以及它们在计算机里的特殊情况。后面每篇的字数应该会比这一篇少很多,也让大家看着不那么费力。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。