用了一天时间,我终于彻底搞懂了 0.1+0.2 是否等于 0.3!

简介: 用了一天时间,我终于彻底搞懂了 0.1+0.2 是否等于 0.3!

网络异常,图片无法展示
|


「这是我参与2022首次更文挑战的第8天,活动详情查看:2022首次更文挑战


本文中涉及到二进制转换和 IEEE 754 标准,所以有时你会看到一堆 000111 或者一些计算机术语,对于非科班的小伙伴可能会有些不友好,但属实都是必要的东西,而且在本文中都进行了相应的讲解。相信我,耐心读完,你一定会有收获的!😀


现象


话不多说,直接上图!


网络异常,图片无法展示
|


通过上图我们知道,答案是不相等,而且还有一个很神奇的问题,0.1+1-1 也不等于 0.1,并且先加后减和先减后加的结果竟然还不一样!


网络异常,图片无法展示
|


JavaScript 到底背着我们干了什么?!


原因


其实这个问题不能完全怪 JavaScript,导致这样的问题是因为 JavaScript 中使用基于 IEEE 754 标准的浮点数运算,所以会产生舍入误差。


也就是说所有遵循 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(二进制) 
复制代码


2.小数部分采用 *2 取整法


0.5 => 0.5*2 = 1 取整 1
0.5(十进制)= 0.1(二进制)
复制代码


0.1 => 0.1*2 = 0.2 取整 0
0.2 => 0.2*2 = 0.4 取整 0
0.4 => 0.4*2 = 0.8 取整 0
0.8 => 0.8*2 = 1.6 取整 1
0.6 => 0.6*2 = 1.2 取整 1
0.2 => 0.2*2 = 0.4 取整 0
0.4 => 0.4*2 = 0.8 取整 0
0.8 => 0.8*2 = 1.6 取整 1
0.6 => 0.6*2 = 1.2 取整 1
...发生循环
得到结果 0.1(十进制)= 00011001100110011001100110011... (0011)循环(二进制)
复制代码


同理,既有整数又有小数的数值进行二进制转换,就是分别对整数和小数部分进行二进制转换,再相加即可。


上面的例子中可以看到 0.1 转二进制会发生无限循环,而 IEEE 754 标准中的尾数位只能保存 52 位 有效数字(具体原因我们稍后讲解),所以 0.1 转二进制就会发生舍入,所以就产生了误差。


在讲解运算过程之前,我们需要 2 个前置知识:


  1. 十进制浮点数转换二进制后尾数的 52 位 有效数字是从第一个 1 开始向后保留 52 位 有效数字,所以接下来你会发现 0.10.2 保留 52 位 尾数后长度会不同。
  2. 在舍入的过程中,遵循 0 舍 1 入 的规则。
  3. 下面的过程中为了方便大家理解,我对所有的保留 52 位 尾数后后面没有 52 位 的情况进行了补零,对部分数字为了方便运算进行了超过 52 位 的补充和转换(比如 1)。


接下来我们一起看一下示例中的运算过程:


0.1
转二进制
0.0001100110011001100110011001100110011001100110011001100110011
保留52位尾数
0.00011001100110011001100110011001100110011001100110011010
0.2
转二进制
0.001100110011001100110011001100110011001100110011001100110011
保留52位尾数
0.0011001100110011001100110011001100110011001100110011010
进行相加
0.00011001100110011001100110011001100110011001100110011010
0.0011001100110011001100110011001100110011001100110011010
----------------------------------------------------------
0.01001100110011001100110011001100110011001100110011001110
相加后的结果保留52位尾数
0.010011001100110011001100110011001100110011001100110100
转十进制
0.30000000000000004
复制代码


接下来是 0.1+1-1 的运算过程:


0.1
转二进制
0.0001100110011001100110011001100110011001100110011001100110011
保留52位尾数
0.00011001100110011001100110011001100110011001100110011010
1
转二进制并保留52位尾数
1.0000000000000000000000000000000000000000000000000000
进行相加
0.00011001100110011001100110011001100110011001100110011010
1.0000000000000000000000000000000000000000000000000000
----------------------------------------------------------
1.00011001100110011001100110011001100110011001100110011010
相加后的结果保留52位尾数
1.0001100110011001100110011001100110011001100110011010
再减1
0.00011001100110011001100110011001100110011001100110100000
转十进制
0.10000000000000009
复制代码


接下来是 0.1-1+1 的运算过程:


0.1
转二进制
0.0001100110011001100110011001100110011001100110011001100110011
保留52位尾数
0.00011001100110011001100110011001100110011001100110011010
1
转二进制并保留52位尾数
1.0000000000000000000000000000000000000000000000000000
进行相减,这里其实等价于 1-0.1 转负数
为了方便相减,我们将 1 的二进制进行转换和补零
0.11111111111111111111111111111111111111111111111111111120
0.00011001100110011001100110011001100110011001100110011010
----------------------------------------------------------
0.11100110011001100110011001100110011001100110011001100110 这里是一个接近 0.9 的负数
相减后的结果保留52位尾数
0.11100110011001100110011001100110011001100110011001101
此时 -0.9+1 等价于 1-0.9
同样,为了方便相减,我们将 1 的二进制进行转换和补零
0.11111111111111111111111111111111111111111111111111112
0.11100110011001100110011001100110011001100110011001101
-------------------------------------------------------
0.00011001100110011001100110011001100110011001100110011
相减后的结果保留52位尾数
0.00011001100110011001100110011001100110011001100110011000
转十进制
0.09999999999999998
复制代码


至此,我们就搞清楚了示例中的运算过程,从而知道了出现这些情况的原因,接下来,我们聊一下 IEEE 754,从而解开:


  1. 什么是尾数位
  2. 为什么是 52 位尾数位
  3. 为什么 0 舍 1 入


以及更多的浮点数神秘面纱~


IEEE 754


IEEE 754 中双精度浮点数使用 64 bit 来进行存储:


  • 第一位存储符号表示正负号 0 正 1 负
  • 2-12位存储指数表示次方数
  • 13-64位存储尾数表示精确度


符号位没有什么可说的,就是用来表示正负数的。


指数位表示次方数,这里的次方数是以当前的进制数为底,比如次方数为 5


  • 如果当前为十进制,就是 105 次方
  • 如果当前为二进制,就是 25 次方


尾数位储存尾数表示精确度,用来表示一个大于等于 1 小于 2 的数值


综上所述,如果我们以 s 表示正负号,h 表示进制数,e 表示次方数,f 表示尾数,则浮点数 value 可以表示为:


value = s*f*h^evalue=sfhe


相信到了这一步,小伙伴们对指数位和尾数位的理解会更清楚一点,也解释了前两个问题。


  1. 尾数位就是 64 bit 浮点数存储尾数的部分,可以表示数值的精确度
  2. 52 位 是在 IEEE 754 标准制度的时候规定如此


而我们上面直接转二进制运算的情况下,实际上是糅合了指数位和尾数位的一个结果,所以我们保留 52 位,是在第一个 1 后面保留 52 位 有效数字。


不知道你有没有发现一个问题: 尾数位只有 52 位,但是我们现在在第一个 1 后面保留 52 位 有效数字,那再加上前面的 1 不就是 53 位 位了吗?


这是因为,尾数部分的整数部分一定是一个 1,那为了充分利用 52 位 空间表示更高的精确度,可以把一定等于 1 的整数部分省略,52 位 都用来表示小数。


最大安全整数


同理,因为只有 52 位 尾数,所以 JavaScript 中的最大安全整数是 2^53-1,其中 5352 位 尾数加上前面省略的 1,而 -1 是因为 2^53 已经是一个边界值了,大于它的值会和它相等,所以最大的安全整数是 2^53-1


网络异常,图片无法展示
|


舍入规则


IEEE 754 标准列出4种不同的方法:


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


第一种规则(也就是默认的舍入方式)可以简单理解为我们常用的 四舍五入,而转化到我们这里的二进制浮点数运算,就是 0 舍 1 入


解决方案


  1. 使用 JavaScript 提供的最小精度值判断误差是否在该值范围内
    Math.abs(0.1 + 0.2 - 0.3) <= Number.EPSILON
  2. 转为整数计算,计算后再转回小数
  3. 保留几位小数 比如金额,只需要精确到分即可
  4. 使用别人的轮子,例如:math.js
  5. 转成字符串相加(效率较低)


参考文档及工具网站


百度百科


维基百科


十进制转 IEEE 754 浮点数二进制


进制转换工具


PS


关于指数计算,科学计数法等,因为和本题不是强相关的知识,为了减少小伙伴们的阅读成本,这里没有介绍。


如有任何问题或建议,欢迎留言讨论!👏🏻👏🏻👏🏻

相关文章
|
2月前
|
JavaScript 前端开发
JavaScript,每5秒打印,已运行时间,和还剩多少时间,多少分,多少秒
JavaScript,每5秒打印,已运行时间,和还剩多少时间,多少分,多少秒
|
4月前
|
存储 算法 索引
1124. 表现良好的最长时间段 (python) 前缀和 分类讨论 最大长度 力扣 面试题
1124. 表现良好的最长时间段 (python) 前缀和 分类讨论 最大长度 力扣 面试题
|
5月前
|
程序员
程序员必知:将时间的秒数转化为分钟数
程序员必知:将时间的秒数转化为分钟数
94 0
|
5月前
1064 朋友数 (20 分) //感觉题目有问题。
1064 朋友数 (20 分) //感觉题目有问题。
|
6月前
|
C语言
编写一个程序, 给出两个时间,计算出两个时间之差,如给出1120表示11:20,1330表示13:30, 将时间间隔以分钟为单位输出。
这是一个C语言程序,它接收两个时间(小时:分钟格式)作为输入,然后计算并输出两个时间之间的差值。代码包括输入处理、时间转换为分钟以及计算时间差。程序运行结果展示了一个具体的示例时间差。
55 0
|
算法 前端开发
前端算法-寻找重复数
前端算法-寻找重复数
剑指offer_数组---数组中出现次数超过一半的数
剑指offer_数组---数组中出现次数超过一半的数
52 0
|
算法 C++ Python
【每日算法Day 90】5种方法:求解数组中出现次数超过一半的那个数
【每日算法Day 90】5种方法:求解数组中出现次数超过一半的那个数
172 0
|
算法 C++ Python
【每日算法Day 91】求解数组中出现次数超过1/3的那个数
【每日算法Day 91】求解数组中出现次数超过1/3的那个数
LeetCode 1394. 找出数组中的幸运数
在整数数组中,如果一个整数的出现频次和它的数值大小相等,我们就称这个整数为「幸运数」。
87 0