用了一天时间,我终于彻底搞懂了 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


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


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

相关文章
|
5月前
|
C语言
【Amazon 面试题1】一个数组,里面得数出现的次数是偶数次,只有一个数出现的次数是奇数次,找出那个出现奇数次的数
本文介绍了解决Amazon面试题的一种方法,即在一个所有数字出现次数都是偶数,除了一个数字出现奇数次的数组中,利用异或运算的性质找出出现奇数次的数字,并提供了C语言实现的代码示例。
77 1
|
6月前
|
SQL 大数据 数据处理
一文搞懂连续问题
**SQL面试中,连续问题涉及窗口函数如row_number()、lag()、sum()over(order by)等,旨在测试综合能力。关键在于特定分组下,为连续内容分配相同分组ID。解题通常分为判断连续条件和后续处理两步。双排序差值法和累积求和法是常见策略。举例来说,连续登录天数、连续点击次数等题目,会在得到分组ID后用聚合函数统计分析。题目难度逐步升级,涉及销售额增长、时间间隔、涨幅条件等,要求灵活应用并处理复杂逻辑。**
|
8月前
|
C语言
编写一个程序, 给出两个时间,计算出两个时间之差,如给出1120表示11:20,1330表示13:30, 将时间间隔以分钟为单位输出。
这是一个C语言程序,它接收两个时间(小时:分钟格式)作为输入,然后计算并输出两个时间之间的差值。代码包括输入处理、时间转换为分钟以及计算时间差。程序运行结果展示了一个具体的示例时间差。
72 0
|
搜索推荐 Cloud Native
【刷题日记】1403. 非递增顺序的最小子序列
【刷题日记】1403. 非递增顺序的最小子序列
|
机器学习/深度学习 人工智能 算法
代码随想录训练营day52| 300.最长递增子序列 674. 最长连续递增序列 718. 最长重复子数组...
代码随想录训练营day52| 300.最长递增子序列 674. 最长连续递增序列 718. 最长重复子数组...
|
存储 JavaScript 前端开发
0.1 + 0.2 不等于 0.3?原来是因为这个
0.1 + 0.2 不等于 0.3?原来是因为这个
237 0
判断一个数是否为4的整数次幂(2的升级版--双份快乐)
判断一个数是否为4的整数次幂(2的升级版--双份快乐)
|
算法
求两个数对应二进制位不同的个数(深度剖析+补充例题)
求两个数对应二进制位不同的个数(深度剖析+补充例题)
190 0
求两个数对应二进制位不同的个数(深度剖析+补充例题)
|
算法 测试技术
【Day27】 LeetCode算法刷题(思路+注释)[801. 使序列递增的最小交换次数 ]
了解算法刷题(思路+注释)[801. 使序列递增的最小交换次数 ]。
130 0
【Day27】 LeetCode算法刷题(思路+注释)[801. 使序列递增的最小交换次数 ]
面试题:判断两个时间是否在同一周
这个题是在面试的时候遇到的,还遇到了2次,和大家分享一下自己的解题思路 感觉像是一个业务上的题,可能面试官刚做过类似的需求,就直接拿出来问了

热门文章

最新文章