这是我参与8月更文挑战的第1天,活动详情查看:8月更文挑战。
前言
这个问题有意义吗?我觉得可能不大,这不就是个浮点数精度问题吗。嗯,确实。之前笔试遇到过这个题,问0.8-0.6===0.2和0.2-0.1 === 0.1 true 还是false,一个小公司,叫中xx龙。我成功记住了他,哈哈哈。现在,来盲目分析一波。
问题
先来看看问题。
0.1+0.2 ===0.3;
//false
0.2-0.1 === 0.1; // 式1
//true
0.8 - 0.6 === 0.2; // 式2
//false
0.1+0.2的问题太经典了,直接来看看下面2个浮点数减法。0.2减去0.1的时候没有精度问题。
再多整几个例子看看,有没有规律可言。
0.4-0.2===0.2; // 式3
//true
0.6-0.3 === 0.3;// 式4
//true
0.8 - 0.7 === 0.1; // 式5
//false
0.8-0.3 === 0.5; // 式6
//true
分别对比式1和式5,式2和式3,可以看出来结果为0.1和0.2的时候有true也有false,说明不是结果的数值决定精度。
对比结果为true的式1,式3,式4,发现式1+式3可以得到式4,两个等式相加仍然得到一个等式,非常合理。同时,当被减数是减数的2倍时,不会有精度问题。
仔细看:式2 + 式4 ==式6?,一个不等式与等式相加,结果居然是一个等式。。。。。。
问题大了。
分析
小数转二进制
整数转二进制是除二取余法
13 .toString(2);
//"1101"
0.25.toString(2);
//"0.01"
小数转二进制则是乘二取整法
0.25=0*2-1 + 1 * 2-2
0.25*2=>0.5;//整数部分为0
0.5*2=>1.0;//整数部分为1,减去1剩余0,结束
小数存储
关于计算机浮点数存储问题,已经有很多人分析过了。
可以查看JavaScript是怎样编码数字的,了解详细内容
浮点数 64位中 : 1位符号位 s,+ 11位指数位 e, 52位小数位f
value = (-1)s(1.f) * 2e
注意:s表示正负,小数部分f是二进制形式,由于f前面自带一个1.,所以value的精度实际上有53位。e表示小数点偏移的位数,有正负,最大可偏移1024
好吧,这个理解后文的基础,不是本文的主题。
小数精度
对0.1采用乘二取整法,会产生无限循环。
由于小数位f只有精度53位,超出部分0舍1入,所以存在精度问题。
0.1.toString(2);
"0.0001100110011001100110011001100110011001100110011001101"
0.2.toString(2);
"0.001100110011001100110011001100110011001100110011001101"
0.3.toString(2);
"0.010011001100110011001100110011001100110011001100110011"
这时候可能就有聪明的金针菇要问了:‘’上面不是说有指数部分还有符号位,这个咋没有呢?‘’
因为上面的String只是表达的通过乘二取整法得到的二进制形式,离计算机存储的格式还有点差别。
当然也不可能用字符串来存储。。。。这里只是表达了一个精度的取舍。
可能又有严谨的小伙伴要问了:“这个小数点后面的位数也不一定是53位啊”
0.1.toString(2).length;
//57
0.2.toString(2).length;
//56
嗯,就算把小数点和前置0减去也得到不53.那么换个思路,既然是精度,就得数小数点后第一个不为0的位到最后一位的长度,对于0.1的二进制形式,把小数点后面的3个0去掉。
"0.0001100110011001100110011001100110011001100110011001101" =>
1100110011001100110011001100110011001100110011001101=>
("1100") * 12 + "1101" => 52位
额,难道又错了?
并没有。还记得前文说了,有一个0舍1入的近似。
已知0.1转换会产生无限循环,循环部分为1100,那么在近似舍入之前,有
("1100") * 12+ "11001100"+ "1100".......
保留53位,看第54位,为1,根据0舍1入规则,先舍去后面部分得到
("1100") * 12 + "11001"
再向前进1位,巧了53位是1,1+1为2,再向52位进1,得到
("1100") * 12 + "11010"
很显然,最末尾得0位没有显示,,就像0.25转换为‘‘0.01’’一样,末尾的0被省去了,不显示了。
所以“0.000"+("1100") * 12 + "1101"就是0.1,精确到53位的0.1。
顺带一提,计算机中存储0.1的形式是符号位s=0,指数位-4(10进制值,代表小数点左移4位)
小数位100+("1100") * 11 + ‘11010’ ,
也即(-1)0*2-4 *(1.1001100110011001100110011001100110011001100110011010)
二进制加减法
现在,我们知道tostring转化后显示的精度正是浮点数的精度,那么开始做加减法吧。
"0.0001100110011001100110011001100110011001100110011001101"//0.1
+
"0.001100110011001100110011001100110011001100110011001101"//0.2
=
// 直接做有点费眼睛,换个形式
0.0(0011)01 //0.1
+
0.0(0110)1 //0.2
=
0.0(1001)11 // 括号里是52位,加上末尾2个1就是54位了,进位再忽视末0
=> 0.0(1001)101
0.0(1001)100 //而0.3是这样的
(0.1+0.2).toString(2);
// "0.0100110011001100110011001100110011001100110011001101"
// 0.0(1001)101
0.1+0.2不等与0.3的原因找到了。
在来做减法
0.0(0110)1 //0.2
-
0.0(0011)01 //0.1
=
0.0(0011)01 //巧了不是,正好53位精度不存在舍入,完美等于0.1
大胆推论:当被减数是减数的2倍时,不会有误差,嗯
不等式 + 等式 = 等式?
实际并不是。。。。。。
式6中0.8-0.3等于0.5的过程中,实际上是存在进位过程的。所以并非严格意义上的等式。
有兴趣可以自己证明。
只要存在进位,就不再准确了。
结论
引用郭冬临的经典语录:你用谎言去验证谎言,得到的也只能是谎言。
还有就是出这种题是真的没意思,难道要人去算到小数点后54位?
写在最后
如发现有错,欢迎指正。 参考 搞懂js中小数运算精度问题原因及解决办法