这是我参与8月更文挑战的第1天,活动详情查看:8月更文挑战。html
这个问题有意义吗?我以为可能不大,这不就是个浮点数精度问题吗。嗯,确实。以前笔试遇到过这个题,问0.8-0.6===0.2和0.2-0.1 === 0.1 true 仍是false,一个小公司,叫中xx龙。我成功记住了他,哈哈哈。如今,来盲目分析一波。segmentfault
先来看看问题。markdown
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的时候没有精度问题。oop
再多整几个例子看看,有没有规律可言。post
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倍时,不会有精度问题。spa
仔细看:式2 + 式4 ==式6?,一个不等式与等式相加,结果竟然是一个等式。。。。。。code
问题大了。orm
整数转二进制是除二取余法htm
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中小数运算精度问题缘由及解决办法