浮點數的相等比較
今天羣裏邊一哥們兒——哦,確切地說,應該是一姊妹——問到了Python中怎樣判斷一個浮點數的小數部分是不是0,本意是要找和C語言中的fmod函數相同功能的函數的,在Python中在math模塊中有這個函數,可是卻挑起了一個浮點數精度的話題。
突然想到偶們學習C語言的時候老師講到過,整數的相等比較可以直接使用==來判斷,但是浮點數的比較不能簡單的用==來比較。那怎麼對浮點數是否相等進行判斷呢?
遂Google了一番,又找到了那個經典的方法:利用差值的絕對值的精度來判斷。
具體就是:f1和f2是兩個浮點數,precision是我們自己設置的精度,比如1e-6。
則可以用 fabs(f1-f2)<=precision 來判斷f1和f2是否相等。
如果要求更高的精度,則把precision定得更小就行了。
網上有一位大牛講,這個方法還存在問題:
“首先,precision是一個絕對的數據,也就是誤差分析當中所說的絕對誤差,使用一個固定的數值,對於float類型可以表達的整個數域來說是不可以的。比如precision取值爲0.0001,二f1和f2的數值大小也是0.0001附近的,那麼顯然不合適。另外對於f1和f2大小是10000這樣的數據的時候,它也不合適,因爲10000和10001也可以真僞是相等的呢。適合它的情況只是f1或者f2在1或者0附近的時候。”
這位大牛給出了這樣的解決方法:
“
既然絕對誤差不可以,那麼自然的我們就會想到了相對誤差
bool IsEqual(float a, float b, float relError ) {
return ( fabs ( (a-b)/a ) < relError ) ? true : false;
}
這樣寫還不完善,因爲是拿固定的第一個參數做比較的,那麼在調用
IsEqual(a, b, relError ) 和 IsEqual(b, a, relError ) 的時候,可能得到不同的結果
同時如果第一個參數是0的話,就有可能是除0溢出
這個可以改造
把除數選取爲a和b當中絕對數值較大的即可
bool IsEqual(float a, float b, relError )
{
if (fabs(a)<fabs(b)) return ( fabs((a-b)/a) > relError ) ? true : false;
return (fabs( (a-b)/b) > relError ) ? true : false;
};
使用相對誤差就很完善嗎?
也不是, 在某些特殊情況下, 相對誤差也不能代表全部
比如在判斷空間三點是否共線的時候,使用判斷點到另外兩個點形成的線段的距離的方法的時候
只用相對誤差是不夠的,應爲線段距離可能很段,也可能很長,點到線段的距離,以及線段的長度做綜合比較的時候,需要相對誤差和絕對誤差結合的方式纔可以
相對完整的比較算法應該如下:
bool IsEqual(float a, float b, float absError, float relError )
{
if (a==b) return true;
if (fabs(a-b)<absError ) return true;
if (fabs(a>b) return (fabs((a-b)/a>relError ) ? true : false;
return (fabs((a-b)/b>relError ) ? true : false;
}
這樣才相對完整。
”
然後這位大牛又給出了一個“最後的比較算法”:
(雖然文章較長,爲了不改變原文的敘述,在這裏將其按照原文錄出)
“
我們先看正數的情況
根據IEEE的內存結構, 指數在高位,尾數在低位
浮點數大的對應的把其內存結構按照整數來理解進行比較的時候,情況也是成立的
因此在這裏如果把他們進行比較的話,作爲整數運算效率會非常的高,比如
float f1 = 1.23;
float f2 = 1.24
f1 > f2 成立
(int&)f1 > (int&)f2 也是成立的
而且,仔細研究IEEE的浮點結構,可以發現在《浮點數比較》當中提到的浮點數精度的問題——不是所有的浮點數都可以精確的表達
可以精確表達的浮點數實際上是有限的,就是那些IEEE的各種情況的枚舉了 2^32個。不能表達的佔據了大多數
IEEE在32位的情況下,尾數是23位的(暗含了第一個位數是1)
對於可以精確表達的浮點數來說,如果我們把這23位當作整數來理解, 它加1,就意味着可以找到比當前對應浮點數大的最小的浮點數了
反之,我們把兩個浮點數,對應的整數做差值運算,得到的整數表明的是兩個浮點數之間有多少個實際可以表達的浮點數(對應的指數相同的情況下很好理解;指數不同的時候,也是同樣有效的)
這樣,對於兩個正的浮點數,他們的大小比較就可以用 (int&)f1 - (int&)f2 來進行比較了
差值的結果實際上就應該是相對誤差了
這個相對誤差,不等同於普遍意義上的相對誤差
它所表達的是,兩個浮點數之間可能還有多少個可以精確表達的浮點數
這樣通過指定這個閾值來控制兩個浮點數的比較就更有效了
對於兩個正的浮點數
bool IsEqual(float f1, float f2, int absDelta)
{
if ( abs ( (int&)f1 - (int&)f2 ) < absDelta ) return true;
}
這裏用abs而不是fabs這在asm上面的運算差距也是很大的了
對於兩個負數進行比較的情況也是相同的
只不過負數內存對應的整數加1,相應的找到的是更小的負數而已
但是負數和整數之間現在還不能進行直接的比較,因爲根據IEEE的內存結構
正數和負數是不同的,對應的整數不能連續
正的最小的數就是0了,對應的整數也是0x00000000
負的最小的數就是-0,對應的整數則是0x 80000000
不用奇怪-0
在IEEE的表達當中是有兩個0的,一個是 +0 一個是-0
有趣的是,按照 f1 == f2 的判斷 +0和-0是相等的
通過對比我們可以發現,
+0 和正的浮點數可以按照轉換成爲整數的方式直接進行比較
-0 和負的浮點數可以按照轉換成爲整數的方式直接進行比較
如果我們能夠把他們連接起來,整個整數方式的直接比較就完備了
對比一下負數的結構, 可以找到一個簡單的辦法了:
把負數內存對應的整數減去 -0 ,他們就連續了
而且更好的結果是,所有的負數經過這次減法後,對應的整數也都是負數了
這樣整個整數比較就變得連續了,而且在整個浮點數範圍內都是有效的了
最後的比較算法就是:
// 函數: bool IsEqual(float f1, float f2, int absDelta)
// 功能:把比較兩個浮點數是否近似相同
// 輸入:f1, f2參與比較的兩個浮點數
// absDelta 兩個浮點數之間允許有多少個其他可以精確表達的浮點數存在,相當於相對誤差
// 輸出: true,兩個浮點數進行相等; false 兩個浮點數不等
// 注意:僅僅適合IEEE 32位浮點數結構
bool IsEqual(float f1, float f2, int absDelta)
{
int i1, i2;
i1 = ( f1>0) ? ((int&)f1) : ( (int&) f1 - 0x80000000 );
i2 = (f2>0) ? ((int&)f2) : ( (int&) f2 - 0x80000000 );
return ((abs(i1-i2))<absDelta) ? true : false;
}
”
這位大牛的講解我暫時還沒有弄懂,暫且先錄在這裏,以後在慢慢讀。別人看是,也清晰明瞭。