1. 程式人生 > >淺談浮點數的比較

淺談浮點數的比較

  由於計算機中採用的是有限位的二進位制編碼,所以浮點數在計算機中的儲存不總是精確的。例如,在經過大量計算後,一個浮點型的數3.14在計算機中可能就儲存成3.140000000001,也有可能儲存成3.1499999999999,這種情況下會對比較操作帶來極大的干擾,所以我們需要引入一個極小數 eps 來對這種誤差進行修正。

1. 等於運算子(==)

在這裡插入圖片描述   如果一個數 a 在 [b-eps, b+eps] 的區間中時,就應當判斷為 a==b 成立。經驗表明, eps 取 10-8是一個合適的數字——對大多數的情況既不會漏判,也不會誤判。因此,我們可以將 eps 定義為常量 1e-8:

const double eps = 1e-8;

為了使比較更加方便,我們可以將比較操作寫成巨集定義的形式:

#define Equ(a, b) ((fabs((a) - (b))) < (eps))

如果想要使用不等於,只需要在使用時的 Equ 前面加一個非運算子 “ ! ” 即可( ! Equ(a, b))。於是在程式中就可以使用 Equ 函式來對浮點數進行比較了:

#include <stdio.h>
#include <math.h>
const double eps = 1e-8;
#define Equ(a, b) ((fabs((a)-(b))) < (eps))
int main() {
double db = 1.23; if(Equ(db, 1.23)) { printf("true"); } else { printf("false"); } return 0; }

對於這個程式,如果寫成 db == 1.23 ,會發現同樣輸出 true。其實像這種比較簡單的比較情況下是可以忽視誤差的(事實上,如果沒有經過容易損失精度的計算,就不需要考慮誤差,可以直接比較),但是當一個變數進行了誤差較大的運算後,精度的損失就不可忽視了。例如下面的程式碼中,db1 和 db2 的精確值都應該是 π ,但是卻輸出了 false ;而使用了 Equ 函式就會輸出 true。

#include <stdio.h>
#include <math.h>
int main() {
	double db1 = 4 * asin(sqrt(2.0) / 2);
	double db2 = 4 * asin(sqrt(3.0) / 2);
	if(db1 == db2) {
		printf("true");
	} else {
		printf("false");
	}
	return 0;
}

顯然,由於這種誤差影響,其他比較運算子也會出現差錯,因此必須進行修正。

2. 大於運算子(>)

在這裡插入圖片描述   如果一個數 a 要大於 b,那麼就必須在誤差 eps 的擾動範圍之外大於 b ,因此只有大於 b+eps 的數才能判定為大於 b (也即 a 減 b 大於 eps)。

#define More(a, b) (((a) - (b)) > (eps))

3. 小於運算子(<)

在這裡插入圖片描述   與大於運算子類似,如果一個數 a 要小於 b ,那麼就必須在誤差 eps 的擾動範圍之外小於 b ,因此只有小於 b-eps 的數才能判定為小於 b (也即 a 減 b 小於 -eps)。

#define Less(a, b) (((a) - (b)) < (-eps))

4. 大於等於運算子(>=)

在這裡插入圖片描述   由於大於等於運算子可以理解為大雨運算子和等於運算子的結合,於是需要讓一個數 a 在誤差擾動範圍內能夠判定其為大於或等於 b ,因此大於 b-eps 的數都應當判定為大於等於 b (也即 a 減 b 大於 -eps)。

#define MoreEqu(a, b) (((a) - (b)) > (-eps))

5. 小於等於運算子(<=)

  與大於等於運算子類似,小於等於運算子可以理解為小雨運算子和等於運算子的結合,於是需要讓一個數 a 在誤差擾動範圍內能夠判定其為小於或者等於 b ,因此小於 b+eps 的數都應當判定為小於等於 b (也即 a 減 b 小於 eps )。

#define LessEqu(a, b) (((a) - (b)) < (eps))

6. 圓周率π

  圓周率 π 不需要死記,因為由 cos(π) = -1 可知 π = arccos(-1)。因此只需要把 π 寫成常量 acos(-1.0) 即可。

const double Pi = acos(-1.0)

最後幾點: ① 由於精度問題,在經過大量運算後,可能一個變數中儲存的0是個很小的負數,這時,如果對其開根號 sqrt ,就會因不在定義域內而出錯。同樣的問題還出現在 asin(x) 當 x 存放+1、acos(x) 當 x 存放-1 時。這種情況需要用 eps 使變數保證在定義域內。 ② 在某些由編譯環境產生的原因下,本應為 0.00 的變數在輸出時會變成 -0.00 。這個問題是編譯環境本身的 bug ,只能把結果存放到字串中,然後與-0.00進行比較,如果比對成功,則加上 eps 來修正為0.00。