如何将十进制转换为二进制
所以100
的二进制表示就是1100100
,大家也可以用windows
自带的程序员计算器多试几个例子
所以0.375的二进制表示为0.011
在看一个稍微特殊的例子:0.1的二进制应该如何表示呢?
可以看出是无限循环的,0.1
的二进制表示为000110011001100...
,于是误差就产生了。
IEEE754标准下下的9.1
二进制表示0.1为:0001100110011001100...
9.1二进制转换后为1001.0001100110011001100...
,写成科学表示法就是:1.0010001100110011001100… x 2³
指数位为3+127 = 130,二进制表示为10000010
由于指数可以是负数,如果算出来的指数不足八位,需要在高位即左边补0至八位。
分数位为0010001100110011001100…
,如果不够23位则在后面补0
正如所看到的,我们首先将 9.1 转换为 IEEE 754 标准,然后将 IEEE 754 值转换为十进制值,得到的却是9.10000038,这就是所谓的浮点误差。
0.1 和 0.2 这两个数值在二进制浮点数中并没有精确的表示,因此,当你将十进制转换成二进制,再将二进制转换成十进制时,就会损失精度。
所以当我们编写一个算法时,应当要十分注意这种浮点数带来的误差。(曾经在国内某个二线大厂的代码中也发现了这种错误😅)
浮点误差的解决方法
在 C++ 中的浮点数精度问题
首先,让我们从一段程序开始这个问题
Copy #include <iostream>
int main () {
float a = 10.0 ;
float b = 0.1 ;
float c = a - b;
std :: cout << "c = " << c << "\n" ;
return 0 ;
}
// 输出结果:9.9
这看似没什么问题,实则问题隐藏于冰山之下。现在,让我们来解开它!
再次尝试下面的程序:
Copy #include <iostream>
#include <iomanip>
int main () {
float a = 10.0 ;
float b = 0.1 ;
float c = a - b;
std :: cout << std :: setiosflags (std :: ios :: fixed) << std :: setprecision ( 23 )
<< "c = " << c << "\n" ;
return 0 ;
}
// 输出结果:c = 9.89999961853027343750000
可以看到,当我们输出小数后 23 位后就能发现这个精度缺失问题了。
第一个例子之所以能得到正确的答案是因为 std::cout 输出流对小数做了四舍五入操作,通过处理给出了正确答案。
看到这,你或许觉得这有什么问题,能得到正确答案不就行了?其实不然,现在让我们将 float a = 10.0;
换为 float a = 500000.0
,我们再次运行程序,看看能得到什么(使用默认输出位数)!
好家伙,懵了吧(500000.0 并不是绝对的,准确来说要确保 a 足够的大)!
很明显,这出现了问题!当我们输出小数后 23 位时你就会得到:
Copy c = 499999.90625000000000000000000
所以,即使你可以通过允许误差 的方式来判断浮点数是否相等(大概类似于使用 a - b <= 1e-12
来近似代替 a - b == 0.0
这样的判别式),你也无法避免输出显示的问题。而这一切都源于浮点数的精度丢失问题。
解决 C++ 中的浮点数精度问题
Boost库
目前,我尝试的解决办法为使用 Boost 库 中的 boost::multiprecision::cpp_dec_float_50
来代替 float
或 double
类型。
Copy #include <iostream>
#include <boost/multiprecision/cpp_dec_float.hpp>
#include <boost/multiprecision/cpp_int.hpp>
namespace mp = boost :: multiprecision; // 命名空间重命名——缩短命名空间
typedef mp :: cpp_dec_float_50 float50; // 变量类型名重命名——缩短变量类型名
typedef mp :: cpp_dec_float_100 float100; // 变量类型名重命名——缩短变量类型名
int main () {
float50 a0 ( "50000.0" ); // 推荐声明方式
float50 b0 ( "10.012" );
float50 c0 = a0 - b0;
float50 a1 ( 50000.0 ); // 因为 50000.0 不是字符串,所以已经出现了精度丢失。
float50 b1 ( 10.012 ); // 而用精度丢失后的数再创建高精度变量已经无意义。
float50 c1 = a1 - b1;
double a2 = 50000.0 ; // 演示精度丢失
double b2 = 10.012 ;
double c2 = a2 - b2;
std :: cout << std :: setiosflags (std :: ios :: fixed) << std :: setprecision ( 25 )
<< "a0\t=\t" << a0 << "\n"
<< "b0\t=\t" << b0 << "\n"
<< "c0\t=\t" << c0 << "\n\n"
<< "a1\t=\t" << a1 << "\n"
<< "b1\t=\t" << b1 << "\n"
<< "c1\t=\t" << c1 << "\n\n"
<< "a2\t=\t" << a2 << "\n"
<< "b2\t=\t" << b2 << "\n"
<< "c2\t=\t" << c2 << "\n\n" ;
return 0 ;
}
/*
a0 = 50000.0000000000000000000000000
b0 = 10.0120000000000000000000000
c0 = 49989.9880000000000000000000000
a1 = 50000.0000000000000000000000000
b1 = 10.0120000000000004547473509
c1 = 49989.9879999999999995452526491
a2 = 50000.0000000000000000000000000
b2 = 10.0120000000000004547473509
c2 = 49989.9879999999975552782416344
*/
很明显,精度丢失问题在 Boost 库中得到了很好的解决。当然,Boost 库还有很多其他的关于浮点数操作的优化,而且基本都是基于源标准库的模式重写的,所以使用起来的非常顺手(注意命名空间即可)。
Kahan 求和
Kahan 求和 是一种补偿的求和算法。代码如下:
Copy float sum ( const std :: vector < float > & a) {
float res = 0 ;
float c = 0 ;
for ( auto y : a) {
y -= c;
float t = res + y;
c = (t - res) - y;
res = t;
}
return res;
}
reference