微信号:tuniutech

介绍:途牛技术中心官方平台

0.1 + 0.2你算对了吗?

2016-06-03 17:33 子非

 详细说说 0.1 + 0.2 != 0.3

本期嘉宾:子非

摘要本文通过描述计算机中浮点数的存储,得出 0.1 并不是 0.1,0.2 也不是 0.2,当然 0.1 + 0.2 也不是 0.3。

几乎在每个计算机语言中,如果去算0.1+0.2,肯定不会得到0.3,甚至有个基于这个答案的域名网站。

要了解为什么0.1 + 0.2不等于0.3,就得回到小数的表示方法上来。在Java中,我们可以使用高精度的BigDecimal来查看实际的值:(其它语言可以更多精确下输出格式,比如%0.100f之类)

import java.math.BigDecimal;

public class Dot3 {

 private static void dump(double d) {
   String s = new BigDecimal(d).toString();
   System.out.format("%s=%s%n", d, s);
 }

 public static void main(String[] args) {
   dump(0.1);
   dump(0.2);
   dump(0.3);
   System.out.println(0.1 + 0.2);
 }

}

运行之后,会输出以下结果:

0.1=0.1000000000 0000000555 1115123125 7827021181 5834045410 15625
0.2=0.2000000000 0000001110 2230246251 5654042363 1668090820 3125
0.3=0.2999999999 9999998889 7769753748 4345957636 8331909179 6875

0.3000000000 0000004

我们可以看到,0.1其实不是0.10.2也不是0.20.3更不是0.3。那么,当实际上的0.1+0.2之后,就只能得到0.3000000000 0000001665 3345369377 3481063544 7502136230 46875。可是,这个数也不是0.3000000000 0000004啊,到底出了什么问题呢?

这一切,得回到数在计算机中的表示中来。在计算机中,数都是以2进制存储的,我们可以得到5

510 = 1012

这是因为,5 = 1 * 22 + 1 * 20。同样,也可以求得小数0.5

0.510 = 0.12

这又是因为,0.5 = 1 * 2-1。类似的,为了求0.1,我们可以不断的乘以2:

0.110, * 2, 0.210 = 0
0.210, * 2, 0.410 = 0
0.410, * 2, 0.810 = 0
0.810, * 2, 1.610 = 1 余 0.6
0.610, * 2, 1.210 = 1 余 0.2,出现循环

所以,0.110 = 0.000112。同理,可以得到,0.210 = 0.00112,0.310 = 0.010012

实际上受精度控制,不能无限循环。在双精度中,存储有效位数是52,加上省去的1,实际上精度是53位。

我们先看看0.1x(或o)表示精度位(下同):

0.1 = 0.0001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 100x 1001 ....
    = 0.0001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010
    = 0x1.999999999999ap-4 (Java 特有表示法)
    = 0.1000000000 0000000555 1115123125 7827021181 5834045410 15625

同理,0.2:

0.2 = 0.0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 00x1 0011 ....
    = 0.0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 010
    = 0x1.999999999999ap-3 (Java 特有表示法)
    = 0.2000000000 0000001110 2230246251 5654042363 1668090820 3125

相加以后:

 +  = 0.0100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1x10
    = 0.0100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1101 00
    = 0x1.3333333333334p-2 (Java 特有表示法)
    = 0.3000000000 0000004440 8920985006 2616169452 6672363281 25
`

0.3为:

0.3 = 0.0100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1x00 1100 ....
    = 0.0100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 11
    = 0x1.3333333333333p-2 (Java 特有表示法)
    = 0.2999999999 9999998889 7769753748 4345957636 8331909179 6875

它们之间的差异,我们可以明显看出:

 δ  = 0.0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 01
    = 0x1p-54 (Java 特有表示法)
    = 0.0000000000 0000005551 1151231257 8270211815 8340454101 5625

再回过头来看之前所说的不一致问题,实际上,他们是同一个数:

= 0.3000000000 0000001665 3345369377 3481063544 7502136230 46875

= 0.0100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1x1
= 0.0100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1101 00

= 0.3000000000 0000004440 8920985006 2616169452 6672363281 25

但是,等等,既然0.1不是0.1,而是0.1000000000 0000000555 1115123125 7827021181 5834045410 15625,为什么显示0.1呢?如果真的想知道的话,请留言,下篇再说(呃,实际上,我也还没弄太明白)。

在本篇的末了,说下正确的计算0.1 + 0.2的方法

// Java
public static String add(String a, String b) {
   return new BigDecimal(a).add(new BigDecimal(b)).toString();
}
# Python
from fractions import Fraction

def add(a, b):
   return float(Fraction(a) + Fraction(b))

至于其它语言,就得自己实现了,基本上是把整数与小数单独拆出来。

文末彩蛋!

为什么大家喜欢520而不是521呢?


可能因为:

5.2010 = 101.0011 0011 0011 0011 …. 2

 
途牛技术中心 更多文章 「牛人讲堂」第二期:有腕儿就是这么任性——邓侃博士与你分享大数据实战精华! 牛人大讲堂第二期总结 ActiveMQ相关介绍及实践 有一群牛人,我们必须知道 产品中心基础知识培训开始啦!
猜您喜欢 WXCOP读书雷达,恭贺新春! Tomcat深入研究文章之三(Tomcat版本与各版本改进) 基于大数据的用户画像构建(理论篇) 这位博赛学员为何如此牛?原来是因为他…… Life is Short! 努力将浪费生命的事情踢走!