0.1 + 0.2 === 0.3 ?

小小的脑袋充满大大的疑惑

关于0.1 + 0.2 === 0.3 ,大家应该都知道这个结果是false

0.2 + 0.3 === 0.5得到的又是true,这到底是咋肥事?

到底哪些计算是成立的?哪些又是不成立的?为什么?如何解决?

带着这些问题开始我们的探究

// 成立的
0.1 + 0.3 === 0.4 // true
0.2 + 0.2 === 0.4 // true
// 不成立的
0.1 * 3 === 0.3 // false
0.2 * 6 === 1.2 // false

什么是IEEE 754?

首先我们要知道,无论是整数还是浮点数,在cpu中都是通过转为二进制计算的,整数转为二进制还是很简单的。那么,如何将浮点数转为二进制数?此时我们就要引入一个关键字:IEEE754(二进制浮点数算术标准)

关于什么是IEEE 754,维基百科中的是这么解释的:

IEEE二进制浮点数算术标准(IEEE 754)是20世纪80年代以来最广泛使用的浮点数运算标准,为许多CPU与浮点运算器所采用。

大部分编程语言都提供了IEEE浮点数格式与算术,但有些将其列为非必需的。

有此可知,不仅是JavaScript,只要是使用了IEEE 754浮点数格式,来存储浮点类型(float 32,double 64)的任何编程语言都有这个问题!下面是我用C++举的例子:

知道了前因,下面我来看一下到底如何把浮点数转为二进制!!!

浮点数转为二进制

首先,JavaScript中的Number是采用64位存储的,这64位由3部分组成,(S:符号位,Exponent:指数域,Fraction:尾数域)。

如下图(从右到左):

综合科学计数法,可以通过以下的计算公式表示:

(-1)s * 1.F * 2E-1023

其中S表示符号,0表示正数、1表示负数

1023为偏移量,32位单精度偏移量位127,下图中的S、P、M只是叫法不同,本文以维基百科中单词(S、F、E)为准。

那么S、F、E如何获得呢?

结算过程

拿263.3举例:

1.首先先获取263的二进制,如下图

可得出263的二进制为:100000111

2.再计算0.3的二进制,如下图

根据规律我们可以看出1001是循环部分,所以得出0.3的二进制为:01001100110011001............

所以263.3的二进制表示为100000111.01001100110011001............

3. 计算S

由于263.3是正数,所有用0表示,故目前64位展示位

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

0

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

4. 计算E

由于要计算为1.F * 2n的形式,所以要把100000111.01001100110011001............ 小数点移到前面,成为1.0000011101001100110011001............

小数点移动了8位,即28

由公式E-1023=8,得出E为1031,我们将1031(1024+7)转为二进制为:

10000000111

目前结果:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

0

1

0

0

0

0

0

0

0

1

1

1

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

_

4. 计算F

目前计算的值为:1.0000011101001100110011001............

将最高位去掉,剩下的填满52位即可

即F为:0000011101001100110011001100110011001100110011001100

因第53位为1,二进制中的四舍五入(零舍一入),故最终结果如下:

0000011101001100110011001100110011001100110011001101

5.最终结果

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

0

1

0

0

0

0

0

0

0

1

1

1

0

0

0

0

0

1

1

1

0

1

0

0

1

1

0

0

1

1

0

0

1

1

0

0

1

1

0

0

1

1

0

0

1

1

0

0

1

1

0

0

1

1

0

0

1

1

0

0

1

1

0

1

大家也可通过通过这个网站直接计算出SEM的值来做验证。

0.1 + 0.2 !== 0.3

回到问题本身,我们可以通过计算0.1、0,2的二进制,经过相加计算就可以知道为什么等式不成立了。

0.1的二进制

通过在 https://babbage.cs.qc.cuny.edu/IEEE-754.old/Decimal.html 中计算得出

2-4*1 .1001100110011001100110011001100110011001100110011010

0.2的二进制

2-3*1 .1001100110011001100110011001100110011001100110011010

计算0.1 + 0.2的二进制

得到值之后该怎么处理呢?浮点数的加法是怎么进行的?那就是先对齐、再计算

对齐以高数位为准,我们此处的例子中-3为高位。因此需要将0.1的二进制处理下,向左移动一位。

移动前:

2-4*1 .1001100110011001100110011001100110011001100110011010

移动后:

2-3*0 .1100110011001100110011001100110011001100110011001101

然后我们计算两个二进制数:

0 .1100110011001100110011001100110011001100110011001101 +

1 .1001100110011001100110011001100110011001100110011010 =

10.0110011001100110011001100110011001100110011001100111

因为我们计算的数的格式为1.xxx,所以小数点要前移一位(此时指数由-3变成了-2),但这样尾数就53位了,根据尾数后一位1进0舍的规则,

移动前:

10.0110011001100110011001100110011001100110011001100111

移动后:

1.0011001100110011001100110011001100110011001100110100

将指数去除,最后的小数位值为:

0100110011001100110011001100110011001100110011001101

根据二进制计算规则,小数点后第一位为0.5,第二位为0.25,第三位为0.125....

由于位数太多,我们写个简单的计算函数帮助我们计算:

		// 二进制小数
    var binaryDecimal = '0100110011001100110011001100110011001100110011001101';
    // 二进制小数点后对位值
    var counterpointArr = [];
    // 小数点后二进制的第一个对位值
    var counterpoint = 0.5;
    for (var i = 0; i < 52; i++) {
        counterpointArr.push(counterpoint);
        counterpoint = counterpoint / 2;
    }
    console.log(counterpointArr) // [0.5, 0.25, 0.125, 0.0625, 0.03125, 0.015625, 0.0078...]
    let sum = 0;
    for (var j = 0; j < binaryDecimal.length; j++) {
        if (binaryDecimal.charAt(j) === '1') {
            sum = sum + counterpointArr[j]
        }
    }
    console.log(sum) // 0.30000000000000004

相关知识点

特殊场景

如何解决

简单的普通场景

思路1

通过差值跟最小精度比较。

Number.EPSILON是 JavaScript 能够表示的最小精度。误差如果小于这个值,就可以认为已经没有意义了,即不存在误差了。

function epsEqu(x, y) {
    return Math.abs(x - y) < Number.EPSILON; //  [ˈepsɪlɒn]
}
console.log(epsEqu(0.1+0.2, 0.3));   // true
console.log(epsEqu(268.34+0.85, 269.19));  // true

思路2

思路就是将小数转为整数,然后再转为小数表示。

//注意要传入两个小数的字符串表示,不然在小数转成二进制浮点数的过程中精度就已经损失了
function numAdd(num1/*:String*/, num2/*:String*/) { 
    var baseNum, baseNum1, baseNum2; 
    try { 
        //取得第一个操作数小数点后有几位数字,注意这里的num1是字符串形式的
        baseNum1 = num1.split(".")[1].length; 
    } catch (e) {
        //没有小数点就设为0 
        baseNum1 = 0; 
    } 
    try { 
        //取得第二个操作数小数点后有几位数字
        baseNum2 = num2.split(".")[1].length; 
    } catch (e) { 
        baseNum2 = 0;
    }
    //计算需要 乘上多少数量级 才能把小数转化为整数 
    baseNum = Math.pow(10, Math.max(baseNum1, baseNum2)); 
    //把两个操作数先乘上计算所得数量级转化为整数再计算,结果再除以这个数量级转回小数
    return (num1 * baseNum + num2 * baseNum) / baseNum; 
};
console.log(numAdd('0.1', '0.2')) // 0.3
console.log(numAdd('268.34', '0.85')) // 269.18999999999994
// 其实还是因为num1 * baseNum 导致的,baseNum为浮点型,先从十进制转为二进制,二进制时丢失了精度
// 解决方法是 return修改为return (num1 * baseNum + num2 * baseNum).toFixed(0) / baseNum;
// 因为我们已经确保了只需要整数位

上面的代码还是比较简单的,但针对大数、特殊场景都没做处理,我们可以使用下面的库

常见库

1.Math.js

专门为 JavaScript 和 Node.js 提供的一个广泛的数学库。支持数字,大数字(超出安全数的数字),复数,分数,单位和矩阵。 功能强大,易于使用。

官网:mathjs.org/

GitHub:github.com/josdejong/m…

2.big.js

官网:mikemcl.github.io/big.js

GitHub:github.com/MikeMcl/big…



 

扔给后端【推荐】

嘿嘿,后端用的也不是float、double,而是用的bigdecimal。

ps.目前组内针对数值计算一律交给后端的。

感谢那些对我有帮助的文档:

https://juejin.cn/post/6844903680362151950

https://babbage.cs.qc.cuny.edu/IEEE-754.old/Decimal.html
https://www.bilibili.com/video/BV1i54y1y7Fn?from=search&seid=16329325986965638794

https://www.bilibili.com/read/cv6776011/

https://segmentfault.com/a/1190000009084877

### 回答1: 执行 `print(0.1 + 0.2 == 0.3)` 的输出结果为 `False`。 这是因为浮点数在计算机内部的表示方式不是精确的,导致计算结果与预期不一致。因此,在比较浮点数的相等性时,应该使用一个误差范围,比如判断它们的差的绝对值是否小于某个阈值。 ### 回答2: 这个表达式输出的结果是 False。这是因为在计算机中,浮点数的表示是有限的,而且存在精度问题。在这个表达式中,0.10.2 都是浮点数,在计算过程中可能会有一些小数位的误差。当我们把0.10.2相加时,得到的结果可能会略微小于0.3,所以这个等式的结果是 False。 要解决这个问题,我们可以使用比较浮点数相等的方法,例如:判断两个浮点数的差的绝对值是否小于一个很小的数。在 Python 中,可以使用 math.isclose() 函数来进行浮点数的相等性判断。所以,正确的比较方式应该是 print(math.isclose(0.1 + 0.2, 0.3)),这样就可以得到正确的结果 True。 ### 回答3: 不等于。因为0.10.2是浮点数,它们在计算机内部以二进制进行存储和计算,在转换过程中可能存在精度损失。所以0.1 + 0.2的结果并不严格等于0.3,而是一个接近0.3的值。因此,print(0.1 + 0.2 == 0.3) 的结果将为False。这是因为浮点数的存储和计算不是完全精确的,我们应该使用其他方法(例如比较两个浮点数之差的绝对值是否小于某个阈值)来判断两个浮点数是否接近相等。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值