AP CSA 1.5 拓展说明:double 与舍入误差
Roundoff Error 是什么?
在 AP CSA 里,roundoff error 指的是:
Java 使用
double存储和计算小数时,因为有些小数不能被电脑精确表示,所以结果可能出现非常小的误差。
它不是随机错误。 同一段 Java 代码重复运行,通常会得到同样的结果。 问题在于:这个结果可能不是数学上的“精确值”,而是一个非常接近的近似值。
为什么小数会不精确?
人类平时使用的是 十进制,电脑底层使用的是 二进制。
二进制的小数位表示的是:
1/2, 1/4, 1/8, 1/16, 1/32 ...
所以像下面这些数,二进制很容易精确表示:
0.5 // 1/2
0.25 // 1/4
0.125 // 1/8
0.75 // 1/2 + 1/4
但是像下面这些数,虽然在十进制里看起来很简单:
0.1
0.2
0.3
1.1
1.2
它们在二进制里可能是无限循环小数。
例如:
0.1₁₀ = 0.00011001100110011...₂
后面的 0011 会一直重复下去。
Java 的 double 不可能存无限长的小数,所以只能存一个非常接近 0.1 的值。这个近似值带来的微小误差,就是 roundoff error。
例子:为什么 0.1 + 0.2 不是刚好 0.3?
System.out.println(0.1 + 0.2);
你可能以为输出是:
0.3
但 Java 可能输出:
0.30000000000000004
原因是 Java 并不是在计算:
精确的 0.1 + 精确的 0.2
而是在计算:
非常接近 0.1 的值 + 非常接近 0.2 的值
所以最后结果也可能只是非常接近 0.3,但不完全等于 0.3。
为什么 (a + b) * (a - b) 可能不等于 a * a - b * b?
数学上:
(a + b)(a - b) = a² - b²
这个恒等式当然是正确的。
但是在 Java 里,如果 a 和 b 是 double,情况可能不同。
例如:
double a = 1.1;
double b = 1.2;
if ((a + b) * (a - b) != (a * a) - (b * b))
{
System.out.println("Mathematical error!");
}
数学上左右两边应该相等。
但是 Java 中:
(a + b) * (a - b)
和:
(a * a) - (b * b)
计算顺序不同。
左边先算:
a + b
a - b
再相乘。
右边先算:
a * a
b * b
再相减。
由于 1.1 和 1.2 本身就不能被 double 精确存储,不同的计算过程会产生不同的微小误差。
结果可能类似这样:
(a + b) * (a - b) -> -0.22999999999999968
(a * a) - (b * b) -> -0.22999999999999976
它们非常接近,但不是完全相等。
所以这个判断可能成立:
left != right
于是程序会输出:
Mathematical error!
如果只有一个小数,会不会也有 roundoff error?
会,只要这个小数不能被二进制精确表示,就可能出现 roundoff error。
例如:
double a = 1.1;
int b = 2;
System.out.println((a + b) * (a - b));
System.out.println((a * a) - (b * b));
虽然 b 是整数,但是参与 double 运算时会被当作 2.0 来计算。
真正的问题来自:
double a = 1.1;
因为 1.1 不能被二进制精确表示。
所以即使表达式里只有一个不精确的小数,也可能导致最终结果出现微小误差。
不过要注意:不是所有小数都会出问题。比如 1.5、0.25 这类可以用二进制精确表示的小数,通常不会造成这种问题。
AP CSA 重点总结
double 适合表示和计算小数,但它表示的是近似值,不是所有小数都能被精确存储。
所以不要直接用 == 或 != 判断两个 double 计算结果是否完全相等。
不推荐:
if (x == y)
更安全的写法是判断它们是否足够接近:
if (Math.abs(x - y) < 0.000001)
{
System.out.println("Close enough");
}
一句话总结:
Roundoff error 就是 Java 用
double存储和计算小数时,由于二进制无法精确表示某些十进制小数而产生的微小误差。