su37josephxia/frontend-interview

Day2 - 0.1 + 0.2 === 0.3 嘛?为什么?怎么解决?

Opened this issue · 18 comments

精度丢失可能出现在进制转换和对阶运算中

JavaScript 使用 Number 类型来表示数字(整数或浮点数),遵循 IEEE 754 标准,通过 64 位来表示一个数字(1 + 11 + 52)

1 符号位,0 表示正数,1 表示负数 s
11 指数位(e)
52 尾数,小数部分(即有效数字)

最大安全数字:Number.MAX_SAFE_INTEGER = Math.pow(2, 53) - 1,转换成整数就是 16 位,所以 0.1 === 0.1,是因为通过 toPrecision(16) 去有效位之后,两者是相等的。
在两数相加时,会先转换成二进制,0.1 和 0.2 转换成二进制的时候尾数会发生无限循环,然后进行对阶运算,JS 引擎对二进制进行截断,所以造成精度丢失。

高逼格ES6
浮点数的运算精度丢失问题。
可以引入ES6中的机器精度Number.EPSILON判定是计算误差还是数据不同。
Number.EPSILON为JavaScript可以表示的最小精度2^(-52)。

浮点数的比较方法错了,正确的方法是比较绝对值是否在JS提供的最小精度范围内

console.log( Math.abs(0.1 + 0.2 - 0.3) <= Number.EPSILON) //true

JS 中的 Number 类型基本符合 IEEE 754-2008 规定的双精度浮点数规则,根据浮点数的定义,非整数的 Number 类型无法用 ==(=== 也不行) 来比较。

console.log( 0.1 + 0.2 == 0.3) //false

浮点数运算的精度问题导致等式左右的结果并不是严格相等,而是相差了个微小的值。

这是浮点数精度丢失的问题,经常出现在进制转换的时候

javascript 使用的是 IEEE-745 浮点数表示法,浮点数在计算机中是使用二进制进行存储,呈现给用户时使用十进制数,当 0.1 和 0.2 转换成二进制时,会出现无限循环,而双精度浮点数的小数部分最多支持 52 位,超过 52 位的部分或被截断,计算机就是使用被截断后的二进制数进行计算,然后在转换成十进制数返回给用户,这个过程就已经出现误差了,而误差的部分就是被计算机截断的部分导致的

解决方法有两种:
1.使用 ES6 的 Number.EPSILON 进行误差判断
2.获取加数中最多的小数位数 e,所有的加数同时放大 Math.pow(10, e) 倍,进行计算之后的结果再缩小 Math.pow(10, e) 倍

不等于,这是由于 JS 存在精度丢失问题。

计算机存储小数的方法是将浮点数转化为对应的二进制存储,采用的标准是 IEEE 754。
JS 用 Number 类型,双精度浮点来表示所有的数字,用 64 bit 进行存储,

1 个符号位,表示正负号;
11 位表示指数,也就是次方
52 位用来表示小数位。

由于在 0.1, 0.2 转化成二进制之后是无限循环的数,所以会被截断,再转化为十进制之后,就会出现误差,导致二者相加不等于 0.3

解决方法:
使用 Number.EPSILON 作为误差判断,因为它是 1 和 比 1 大的最小浮点数的差值(2^-52),
只要符合:
Math.abs(0.2 - 0.3 + 0.1) < Number.EPSILON,则证明 0.1 + 0.2 === 0.3

回答

不等于

原因

这是由于 JS 存在精度丢失问题。
JavaScript 存储数值是以双精度浮点数来存储的,采用的标准是 IEEE 754。
用双精度浮点数表示小数可能会得到一个无限循环的数,无限循环二进制转换为 10 进制会出现误差;
这里的 0.1+0.2 就是出现了这种误差

解决思路

解决本题

方法 1:可以将小数* $10^n$ 转换为整数,整数不存在精度丢失问题;
方法 2:使用 Number.EPSILON 作为误差判断

解决同类型题

方法:将数字转换为字符串,字符串逐位相加得到精确的结果;

最后

感谢 @liangle @rachern @bianzheCN 让我学到了 Number.EPSILON

这是浮点数的精度问题,JavaScript中的数字都是保存为双精度浮点数,在十进制转化成二进制的时候,不能被2除尽的数都无法精确表达,然后进行了截取。简单来说就是十进制转二进制计算这个过程是不完全准确的。计算时可以放大10的n次方转换成整数再进行计算。

0.1 + 0.2 是不等于 0.3的,这是浮点数的精度的问题;
为了实现计算,极小数和极大数通常用科学计数法表示;
因为js始终遵循国际IEEE754标准,将数字存储为双精度浮点数;
其中数字存储在位 0 到 51 中,指数存储在位 52 到 62 中,符号存储在位 63 中
我们按 IEEE754 标准,将十进制的 0.1 转换为二进制的 0.1;
十进制小数转换成二进制小数采用"乘2取整,顺序排列"法;
具体做法是:用2乘十进制小数,可以得到积,将积的整数部分取出,再用2乘余下的小数部分,又得到一个积,再将积的整数部分取出,如此进行,直到积中的小数部分为零,在这里0.1和0.2是无法让积中的小数部分为零,所以是一个无限循环的数。
再根据双精度标准,我们将把其四舍五入到 52 位时会丢失精度,计算完再转回十进制时和理论结果不相同。

  • 不等于
  • 0.1+0.2 在计算中会先转换为二进制,在转换过程中0.1和0.2都会转换为无限循环的小数,相加之后再转换回十进制,之后就会产生误差
  • 解决办法我的思路是转换成整数相加,加完再转回小数

在js中,0.1 + 0.2不等于0.3。
这个语言现象,其实不止在js里面会有,Java也会有同样的问题。具体原因:是因为语言的底层实现里面,他遵照的是IEEE 754这个标准,用浮点数去表示数字,像0.1/0.2这种数字其实是没有办法精确的用浮点数来表示出来的。
那么在做0.1 + 0.2的加法运算时,其实是做最接近于他们的浮点数表示的加法运算,那么加出来肯定不是0.3。
解决方式:先把被加数转化成整数,做好相加之后再转回小数,这样保留他们的精度。

  1. 不等于

  2. 为什么

    js的浮点数精度丢失

    解析

    js 里的 Number 类型 遵循IEEE 754的标准:是一个 64 位的固定长度的浮点数, 第一个位置代表 正负,后52位是尾数 代表二进制的有效的数字 中间的11位用于指数

    因为有效数字是52位的,所以当数值转化成二进制的时候如果尾数大于52为的时候,会出现溢出, 就会按照IEE754的规则进行舍入(最近偶数)

    回到0.1 + 0.2 的问题,计算过程是先转二进制 进行相加 结果再转成我们看到的十进制
    0.1转化成二进制 通过 不断的取小数部分 乘 2,可以看出会无限循环下去,会出现溢出 大于52位 会按照规则舍入, 0.1 ~ 0.9 中只有0.5不会出现精度丢失的问题,其他的都会出现精度丢失的问题

    为什么 0.3 === 0.3 是true : 小数值会去16位精度 0.3 实际上可能是 0.300000000000000005, 也可能是0.3000000000000000051 取16 + 省略的一位位精度会相同

0.3 === 0.3 0.2 + 0.2 === 0.4 这个咋理解

  1. 怎么解决 精度丢失的问题
    1. 项目中没有过多计算的时候,会自己通过 * 1000 转成整数 除以 1000 配置 toFixed()展示
    2. 有较多数值计算的时候,采用现有处理好的库 mathjs 之类的

肯定是不等于的

  • 在计算的时候会先转换为二进制,小数采用乘二取整方法获得二进制,象0.1就会出现无限小数
  • 又因为js number是64位双精度浮点类型,超过最大安全数时会被截取,导致二进制进位
  • 最终计算完转为10进制的结果不为0.3

解决方法

  • 转为整数计算完再转浮点数
  • BigInt
  • 第三方库(Math.js,Sinful.js,BigDecimal.js等) - 同学总结
  • 判断等式两边相减是否小于Number.EPSILON -同学总结
  • 转字符串运算(x.toPrecision() + y.toPrecision()) - 同学总结

结论:

0.1 + 0.2 !=0.3

原因:

JavaScript中的数字,是以双精浮点数在计算机中进行储存的,为二进制,占用64bit,既用64位数来表示一个数值。
而实际呈现出的数字,是以十进制的方式呈现的,这中间就涉及十进制到二进制的转化。

而0.1,0.2转化为二进制时,会出现无限循环的问题,所以由于位数限制会进行截断,而用截断过后的二进制数转换回十进制表示出来,就会自然出现精度缺失。

解决办法:

  • es6:Number.EPSILON属性表示浮点数运算的最小精度差,比较浮点数时,结果的差值少于这个值,即可认为判断相等

  • 通用:若不只是判断,是要保证计算结果精度的话,计算加法时,判断出两数最大小数点后位数,并依此共同转化为整数进行计算即可,在整数小于Math.pow(2,53)计算时,无精度缺失问题。

不等,是因为 JavaScript 中使用基于 IEEE 754 数值的浮点计算,所以会产生舍入误差。

IEEE 754

IEEE 754 中双精度浮点数使用64 bit来进行存储:
第一位存储符号表示正负号 0 正 1 负
2-12位存储指数表示次方数
13-64位存储尾数表示精确度

小数在计算机中的存储方式:
小数(浮点数)会被转成对应的二进制数,并用科学计数法表示,
然后把这个数值数值通过 IEEE 754 标准表示成真正会在计算机中存储的值。

具体原因

0.1和0.2转换成二进制后是一个无限循环的二进制数,而尾数位只能存储52位有效数字,所以这个时候无法被存储的后续部分就进行了取舍,取舍的规则是在 IEEE 754中定义的。

为什么无限循环

转换二进制的过程:

  1. 整数部分模2取余法
    3 => 3%2 = 1 余 1
    1 => 1%2 = 0 余 1
    3(十进制)= 11(二进制)

4 => 4%2 = 2 余 0
2 => 2%2 = 1 余 0
1 => 1%2 = 0 余 1
4(十进制)= 100(二进制)

  1. 小数部分模2取整法

0.5 => 0.5*2 = 1 取整 1
0.5(十进制)= .1(二进制)

0.1 => 0.12 = 0.2 取整 0
0.2 => 0.2
2 = 0.4 取整 0
0.4 => 0.42 = 0.8 取整 0
0.8 => 0.8
2 = 1.6 取整 1
0.6 => 0.62 = 1.2 取整 1
0.2 => 0.2
2 = 0.4 取整 0
0.4 => 0.42 = 0.8 取整 0
0.8 => 0.8
2 = 1.6 取整 1
0.6 => 0.6*2 = 1.2 取整 1
...发生循环

浮点数的舍入

任何有效数上的运算结果,通常都存放在较长的寄存器中,当结果被放回浮点格式时,必须将多出来的比特丢弃。有多种方法可以用来执行舍入作业,实际上IEEE标准列出4种不同的方法:

  • 舍入到最接近:舍入到最接近,在一样接近的情况下偶数优先(Ties To Even,这是默认的舍入方式):会将结果舍入为最接近且可以表示的值,但是当存在两个数一样接近的时候,则取其中的偶数(在二进制中是以0结尾的)。
  • 朝+∞方向舍入:会将结果朝正无限大的方向舍入。
  • 朝-∞方向舍入:会将结果朝负无限大的方向舍入。
  • 朝0方向舍入:会将结果朝0的方向舍入。

00011001100110011001100110011... (0011)循环

0(0011)(0011)(0011)(0011)(0011)(0011)(0011)(0011)(0011)(0011)(0011)(0011)(0011)(0011) = 0(0011)(0011)(0011)(0011)(0011)(0011)(0011)(0011)(0011)(0011)(0011)(0011)(0011)

所以 0.1 和 0.2 存储后就发生了精度丢失的问题,从而相加之后的结果不严格等于 0.3。

解决方案

  1. 使用 JavaScript 提供的最小精度值比较是否相等 => Math.abs(0.1 + 0.2 - 0.3) <= Number.EPSILON
  2. 保留几位小数 比如金额,只需要精确到分即可
  3. 使用别人的轮子,例如:math.js
  4. 转成字符串相加(效率较低)

十进制转二进制方法是"乘以2取整,顺序输出”;

计算机存储浮点数,会被转为二进制,0.1和0.2会被转为无限不循环小数,直到存满规范的浮点数存储空间,相加之后,尾部大小溢出,取和,最终变成0.30000000000000004

解决办法,使用第三方库,math.js 或者big.js,自己来计算的话也可以先将浮点类型转换为整形,最终再对应处理相加之后转回浮点型。

昨天写错地方了,挪过来

JS中的所有数字均用浮点数值表示,其采用IEEE 754标准定义64位浮点格式表示数字。

其表示方式为1位符号位,11位指数位,52位小数位。

js的表示的数字范围是由这些64位组成的。由于0.1和0.2用二进制表示是无限循环小数,而JS的浮点数计数标准中浮点数小数位只有52位,所以会有精度的丢失。我们知道在10进制计算过程中我们是四舍五入,而在二进制中只有0和1,所以是1进0舍,所以0.1+0.2的结果的偏差会照成实际结果要比0.3略大。
如何避免呢,原则上是无法避免的,这和某种语言无关,这个是和我们的储存方式相关。所以一般我们采用的解决方案是使用别人写好的库,比如'math.js'等

不等于
js 计算精度丢失问题
0.1+0.2 是转换给二进制进行存储的
0.1转换给二进制的结果是由0和1组成的无限小数
0.2也是超出计算精度,结果保留十六位小数
0.1在计算机内部不是精确的0.1
0.2在计算机内部不是精确的0.2
所以出现了0.1+0.2 不等0.3的情况

console.log(
    (424325.2).toString(2), (42432225.2).toString(2),
    (424325.2).toString(2).length, (42432225.2).toString(2).length
)

image

为什么他们的长度不一样?按照52尾数来算,打印结果不应该都是54吗?