一、0.01相加100次等于几?
你是不是脱口而出,结果是 1 ,现实中确实是这样,那计算机中呢,也是 1 吗?那我们来实验一下。我们可以看到最后的结果竟然不等于 1 。
double sum = 0;
for (int i = 1; i <= 100; i++) {
sum += 0.01;
}
System.out.println("sum = " + sum); //sum = 1.0000000000000007
出现这种现象的原因,既不是程序编写错误,也不是计算机出现了故障,当然更不是开发语言本身的问题。如果你知道计算机是如何处理小数的,就能明白出现这样的错误是理所当然的。那么,计算机到底是如何处理小数的呢?
二、如何用二进制表示小数
我们知道计算机是用二进制表示整数的,那么对于小数当然也是一样了,但是,用二进制表示小数与表示整数有很大的差别。
在了解计算机用二进制表示小数的具体方法之前,我们先来做一下热身准备,尝试将1011.0011这个二进制小数转换成十进制。小数部分应该如何转换呢?和整数一样,将各位数字乘以对应的位权后相加即可。
在二进制中,整数部分的位权,第1位是2的0次幂,第2位是2的1次幂,以此类推。小数部分的位权,第1位是2的 - 1次幂,第2位是2的 - 2次幂,以此类推。从0次幂开始,高位方向的位权是按1次幂、2次幂这样的方式递增的,因此低位方向的位权自然要按 - 1次幂、- 2次幂这样的方式递减。不仅是二进制,十进制和十六进制里也是一样的。在二进制中,第3位小数代表2的 - 3次幂(0.125),第4位小数代表2的 - 4次幂(0.0625),因此,小数部分的 .0011转换成十进制数就是0.125 +0.0625 = 0.1875。整数部分1011转换成十进制数是11,因此二进制数1011.0011转换成十进制数就是11 + 0.1875 = 11.1875。
三、计算机出错的原因
知道了二进制小数转换成十进制数的方法,我们就可以理解计算机为什么会计算错了。先说一下答案,原因是有一些十进制小数无法准确转换成二进制数。例如,十进制数0.1就无法用二进制数来准确表示,即使用几百位小数也表示不了。接下来就讲一讲造成这一现象的原因。
4位二进制小数能表示的数值范围是0.0000~0.1111,因此能表示的小数只能是0.5、0.25、0.125、0.0625这4个位权本身及其相加的排列组合。这些数值的排列组合能表示的数如下图所示,我们可以发现这些十进制数是不连续的。
从上图可以看出,在十进制数中,0的后面就是0.0625,也就是说,这两个数之间的数,都不能用4位二进制小数来表示,而0.0625的下一个数一下子就到了0.125。虽然我们可以通过增加二进制小数的位数来增加与之对应的十进制数的个数,但无论增加多少位小数,都无法通过让2的负 ×× 次幂相加来凑出0.1。实际上,将十进制数0.1转换成二进制数,会得到0.00011001100…(之后1100不断重复)这样一个循环小数。这和十进制小数无法准确表示1/3是一样的道理,1/3只能用0.3333…这样的循环小数来表示。
说到这里,大家应该能明白为什么0.01相加100次的程序无法得出正确的计算结果了吧。无法准确表示的值就只能使用近似值来表示。计算机能力有限,无法处理无限的循环小数,只能根据变量所对应的数据类型的比特数,对数值进行截断或者采取四舍五入的处理。因此,如果将0.3333…这个循环小数从中间截断变成0.333333,也会产生同样的问题,将它乘以3的结果也不是1(而是0.999999)。
4.对于高精度要求的小数计算如何解决呢?
1)使用高精度计算库:传统的浮点数计算可能会产生舍入误差,可以使用高精度计算库来处理小数运算,例如Java中的BigDecimal
类。
2)四舍五入:在进行小数计算时,可以将结果四舍五入到适当的位数。这样可以减少舍入误差的影响。
3)使用整数运算,比如我们要计算0.01相加100次后的结果,我们可以先将0.01 × 100 = 1,之后将1相加100次,得到100,再将100 ÷ 100,也就是1,这样也就不会出问题了。
注:参考书籍《程序时怎样跑起来的》《Java编程的逻辑等》