谈谈二进制(一)

阅读:3136

发布时间:2020年4月19日 21:10

编程技巧
# 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](https://0.30000000000000004.com/)***]统计了各个语言中`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. 结尾 这篇文章字数有点多,能看到这里的都是真的猛士。接下来可能还会再写个两到三篇关于二进制的系列文章(计划),后面会讲一下二进制的一些运算,以及它们在计算机里的特殊情况。后面每篇的字数应该会比这一篇少很多,也让大家看着不那么费力。