9.6 浮點數誤差
上一節種下的因,這一節收成。浮點數的誤差來自兩個層面,得分開認識。
¶誤差一:存進去的那一刻就不準了
尾數只有 52 位元,兩件事註定發生:
(1)多數小數存不準。 0.1 存進去就變 0.10000000000000000555\ldots(上一節示範過)。更驚人的是,誤差會在運算後浮上檯面:
#include <iostream>
#include <iomanip>
using namespace std;
int main() {
double a = 0.1 + 0.2;
cout << (a == 0.3) << '\n'; // 你以為是 1……
cout << fixed << setprecision(20);
cout << a << '\n'; // 掏出家底看看
cout << 0.3 << '\n';
return 0;
}
執行結果:
0
0.30000000000000004441
0.29999999999999998890
0.1 + 0.2 和 0.3 各自帶著不同的儲存誤差,加完根本是兩個不同的數——用 == 比較浮點數,從此列為禁招(正確做法馬上講)。
(2)太大的整數也存不準。 52 位元尾數能精確表示的整數只到 2^{53}(約 9 \times 10^{15});再往上,浮點數的「格子」間距比 1 還大,相鄰整數開始黏在一起:
#include <iostream>
#include <iomanip>
using namespace std;
int main() {
double x = 9999999999999999.0; // 16 個 9,超過 2^53 了
cout << fixed << setprecision(0);
cout << x << '\n';
return 0;
}
執行結果:
10000000000000000
16 個 9 存進 double,印出來變成 10^{16}——它被「吸」到最近的可表示格子上了。這正是 9.4 那個 1e18 + 1 消失事件的原理,也是 9.10「大整數別碰浮點數」的鐵證。
¶誤差二:每一步運算都可能再磨掉一點
就算存得準,運算的結果也可能需要捨入。單步誤差極小(10^{-16} 等級),但迴圈會放大它:
#include <iostream>
#include <iomanip>
using namespace std;
int main() {
double a = 1e9;
for (int i = 0; i < 1000000; i++) {
a -= 1e-6; // 從 10^9 扣一百萬次 10^-6
}
cout << fixed << a << '\n'; // 數學上應是 999999999 整
return 0;
}
執行結果(本站評測環境):
999999999.046326
數學上答案是 10^9 - 1 = 999999999,實際卻多了 0.046——每次相減,10^{-6} 這個本來就存不準的數在 10^9 的大數旁邊又被磨掉一些,一百萬次累積下來就看得見了。而且不同電腦、不同編譯環境跑出來的值可能不一樣——這也是浮點數題目常附誤差容忍的原因。
¶正確的比較方式:EPS
動手試試看:寫程式驗證 0.5 + 0.25 == 0.75 是否成立,再對照 0.1 + 0.2 == 0.3。回想上一節的動手試試看,解釋為什麼一個成立、一個不成立。