什么, 0.3 - 0.2 ≠ 0.1 ?

标签: 公众号文章算法


惨痛的历史教训

记得还在上学那会儿,给咱们上《运筹学》的老师留了一个课程实验,就是让咱们每一个人都去实现一个书中所讲的算法。因为当时彻底没有什么分层、模块化的啥概念,写代码就是一股脑往里塞逻辑,写出来的代码用一坨形容彻底不为过。编程

当我实现完算法以后,开始弄几个值做为输入进行测试,发现有的值能够测试成功,有的却不行,怎么办呢?调试呗,面对着那么一大坨乱糟糟的代码,调试简直是灾难,好像花了整整一下午加晚上的时间去调试,调试的我两眼冒金星,脖子转不动,真是:写代码一时爽,调试火葬场bash

最后我居然发现了一个神奇的现象(也是后来一直铭记的教训):编程语言

0.2 - 0.1 == 0.1 这个表达式的结果为true,可是0.3 - 0.2 == 0.1的这个表达式的结果居然为false,我滴个乖乖,真不敢相信本身的眼睛。后来查了查书说是浮点数表示的数字并非精确的,到底怎么个不精确法,本篇文章就来唠叨唠叨~模块化

浮点数究竟是怎么表示的?

浮点数实际上是用来表示小数的,咱们平时用的十进制小数也能够被转换成二进制后被计算机存储。好比9.875,这个小数能够被表示成这样:测试

9.875 = 8 + 1 + 0.5 + 0.25 + 0.125 
      = 1 × 2³ + 1 × 2⁰ + 1 × 2⁻¹ + 1 × 2⁻² + 1 × 2⁻³ 
复制代码

也就是说,若是十进制小数9.875转换成二进制小数的话就是:1001.111。为了在计算机里存储这种二进制小数,咱们统一把它们表示成a × 2ⁿ的科学计数法的形式,其中1≤|a|<2,好比1001.111能够被表示成1.001111 × 2³。咱们把小数点以后的001111称为尾数,把中的3称为指数,又由于一个数字有正负之分,因此为了表示一个小数,只须要下边这几部分就能够了:spa

  • 符号部分。设计

  • 尾数部分。调试

  • 指数部分。code

根据表示尾数和指数所使用的存储空间大小不一样,浮点数又被具体细分为:

  • 单精度浮点数(通常编程语言中的float类型):

    单精度浮点数总共占用4个字节:

    • 使用1个比特位表示符号部分,值为0时表示正数,值为1时表示负数。

    • 使用8个比特位表示指数部分。

    • 使用23个比特位表示尾数部分。

    画个示意图就是这样:

    image_1df37645t1v4mml1eo3t3u1v04m.png-78.2kB

  • 双精度浮点数(通常编程语言中的double类型):

    双精度浮点数总共占用8个字节:

    • 使用1个比特位表示符号部分。

    • 使用11个比特位表示指数部分。

    • 使用52个比特位表示尾数部分。

    示意图就不画了。

对于某个使用科学技术法表示的二进制小数,直接把指数对应的数字和尾数对应的数字填到对应的位置就完了么?好比对于二进制小数1.001111 × 2³(也就是十进制的9.875),若是用单精度浮点数表示的话应该是这样么(为了清楚的表示各个部分,咱们用空格把各个部分分隔开,其实是没有间隔的):

0 00000011 00111100000000000000000
复制代码

其中:

  • 第一个比特位0表示这是一个正数。

  • 00000011表示指数3

  • 00111100000000000000000表示尾数001111

哈哈,事实并无这么简单,设计浮点数的大叔出于某种目的而把事情搞的稍微有些复杂(这个复杂是针对咱们人类说的)。他们把指数部分看成一个无符号数,这个无符号数的字面量并非真实的指数值,而是通过一些曲折的计算手段才能获得最后的指数值。他们把浮点数的存储方式分为了3种状况讨论:

  • 当指数部分的比特位既不全为0(数值0),也不全为1(单精度时为255,双精度时为2047)时:

    这时真实的指数值等于指数部分的字面量减去一个偏置值,这个所谓的偏置值在单精度浮点数中是127,在双精度浮点数中是1023

    比方说咱们想使用单精度浮点数来存储二进制小数1.001111 × 2³,它的指数值为3,咱们须要让指数部分的字面量减去127的值为3,因此指数部分的字面量就是130,用二进制表示就是:10000010。尾数部分是001111,因此表示二进制小数1.001111 × 2³的真正的单精度浮点数形式就是:

    0 10000010 00111100000000000000000
    复制代码

    小贴士: 这是为毛呀?为啥要在字面量的基础上减去一个所谓的偏置值。其实设计浮点数的大叔是这样考虑的,对于表示指数的一堆比特位来讲,它们总共能表示的数字个数是肯定的,比方说单精度浮点数的指数部分占用8个字节,那么就总共能表示2⁸个数字,也就是256个数字,不过比特位全为0和全为1时有特殊用途,因此指数部分最多能表示254个数字,能表示的数字范围就是-126~127。他们想让做为无符号数字面量最小的那个二进制数,也就是00000001来表示这254个数字中最小的那个,也就是-126,而后随着字面量的增大,表示的真实指数值也逐渐增大,直到最大的字面量11111110来表示真实的指数值127。这样只须要在字面量的基础上减去127就能达到这个效果。相似的,对于双精度浮点数来讲,就得在指数部分的无符号字面量的基础上减去1023才能获得真实的指数值。看到了吧,设计浮点数的大叔只是想让字面量越大,真正的指数值越大这个目的才引入了偏置值这个怪怪的概念。

  • 当指数部分的比特位都为0时:

    这是一种特殊的状况,此时的指数并不表明0,而表明1减去偏置值,对于单精度浮点数来讲,也就是指数表明1 - 127 = -126

    不过上一种状况中不是已经表示了指数值为-126的状况了么,为啥还要把这这个指数值为-126的状况单独提出来呢?这个还得从咱们表示1.001111 × 2³这个二进制小数的例子提及,别忘了咱们在浮点数的尾数部分存储的是001111,也就是自动忽略了小数点左边的那个1,这样就节省了1个比特位。不过在指数部分全为0的状况下,尾数部分是不包含小数左边的那个1的,比方说有一个单精度浮点数:

    0 00000000 01010000000000000000000
    复制代码

    这个浮点数表示的二进制小数就是:0.0101 × 2-¹²⁶

    能够看到,当单精度浮点数的尾数部分的比特位都为0时,表示的都是比1 × 2-¹²⁶小的数字,也就是接近0的那部分数字。

    当浮点数尾数部分的比特位都是0时,能够表示数值0.0,不过因为符号位(就是第一个二进制位)的存在,因此有+0.0-0.0之分。

  • 当指数部分的比特位都为1时:

    此时能够再细分为两种状况:

    • 当尾数部分的比特位都是0时:

      此时表示一个无穷大的值,当符号位为0时,表示正无穷大,当符号位为1时,表示负无穷大。

    • 当尾数部分的比特位不都为0时:

      此时表示一个NaN值,NaN的全称就是Not a Number,也就是不表明一个数,这在某些状况下是有用的。

至此,浮点数存储方式的三种状况就唠叨完了。不知道你们有没有发现一个规律,就是在不考虑符号位时,假设咱们把浮点数的其他比特位当作一个无符号数,那么这个无符号数的值越大,它所表示的浮点数值也越大,这样在作浮点数比较大小操做时便十分简单。

再看浮点数运算

看完浮点数的存储格式以后,咱们再回过头看一下最初提出的0.2 - 0.1 == 0.1值为true,而0.3 - 0.2 == 0.1值为false的状况。咱们以单精度浮点数为例,看一下这些表达式里涉及到的这些数字该如何表示:

  • 0.1

    十进制小数0.1没法转成尾数在23位之内的二进制小数,因此只能通过舍入,获得近似值:

    1.10011001100110011001101 × 2⁻⁴
    复制代码

    指数值是-4,根据咱们上边所述的第一种状况,指数部分的字面量就是123,表示成二进制小数就是01111011,因此咱们能够获得十进制小数0.1对应的单精度浮点数就是:

    0 01111011 10011001100110011001101
    复制代码
  • 0.2

    它的二进制小数近似值就是:

    1.10011001100110011001101 × 2⁻³
    复制代码

    同理,能够获得以下单精度浮点数

    0 01111100 10011001100110011001101
    复制代码
  • 0.3

    它的二进制小数近似值就是:

    1.00110011001100110011010 × 2⁻²
    复制代码

    同理,能够获得以下单精度浮点数:

    0 01111101 00110011001100110011010
    复制代码

那么:

  • 计算0.2 - 0.1的值

    就至关于计算:

    1.10011001100110011001101 × 2⁻³ - 1.10011001100110011001101 × 2⁻⁴
    复制代码

    获得的结果就是:

    1.10011001100110011001101 × 2⁻⁴
    复制代码

    而这个值正好是十进制小数0.1的二进制小数表示形式,因此0.2 - 0.1 == 0.1这个表达式的结果就为true

  • 计算0.3 - 0.2的值

    就至关于计算:

    1.00110011001100110011010 × 2⁻² - 1.10011001100110011001101 × 2⁻³
    复制代码

    获得的结果就是:

    1.10011001100110011001110 × 2⁻⁴
    复制代码

    而这个值并非十进制小数0.1的二进制小数表示形式,因此0.3 - 0.2 == 0.1这个表达式的结果就为false

    这种计算结果的差别主要是由于十进制小数转换为二进制小数须要很是多的比特位,甚至转为的二进制小数是无限小数,而使用浮点数来表示二进制小数时使用的存储空间是有限的,必须进行必定程度的舍入操做,这样表示的二进制小数就不精确,采用浮点数进行运算的结果就不精确,你们在平常使用浮点数的过程当中要多加注意。

题外话

写文章挺累的,有时候你以为阅读挺流畅的,那实际上是背后无数次修改的结果。若是你以为不错请帮忙转发一下,万分感谢~ 这里是个人公众号「咱们都是小青蛙」,里边有更多技术干货,时不时扯一下犊子,欢迎关注:

相关文章
相关标签/搜索