【题目来源】
刘汝佳《算法竞赛入门经典 第2版》第一章习题——问题3:double型浮点数最大正数值和最小正数值分别是多少(不必特别精确)?
书中并未给出标准答案,这道题应该怎么解呢?老金还着实费了不少脑细胞。
一、跑到海枯石烂的代码
网上查到这样的代码:
#include <stdio.h>
int main(){
double i=0.0000000000000001;
for(;i>0;i+=0.0000000000000001){
}
printf("%.100lf\n%.100lf\n",i,i-1);
return 0;
}
实际测试了一下,运行到海枯石烂也没等到结果。如果有人等到结果还望告知。
从代码看,其逻辑应该是数值超限时数值的符号会变号,由正数变成负数。
鉴于有这么虐人的运行时间,估计出题者的标准答案肯定不是如此。
二、利用头文件宏获取
在float.h头文件中,定义了两个宏:
DBL_MAX:double类型能够表示的最大有限正数。
DBL_MIN:double类型能够表示的最小正规化正数值(可以理解为比最小正数大,但接近于最小正数的值)。
#include <stdio.h>
#include <float.h>
int main() {
printf("最大正数值: %g\n", DBL_MAX);
printf("最小正规化正数值: %g\n", DBL_MIN);
return 0;
}
结果输出:
最大正数值: 1.79769e+308
最小正规化正数值: 2.22507e-308
这应该是比较靠谱的数据了,这个值真的是很大很大,所以在实际编程时,完全没有必要考虑数值超限的问题。
因此,浮点数的主要问题是误差,而不是数值超限。
三、老金的半自动步枪
老金经过尝试,可以用一种“编程+人工确认”的半自动输出方法。虽然是半自动方法,但是非常简单高效。
因为要求最大正数和最小正数,数字肯定会非常大,所以要用到科学记数法表示。科学记数法表示的数分为基数部分和指数部分,只要分别确定这两部分,这个数也就确定了。
1. 求最大正数值
(1) 求最大正数值的指数部分
思路是将基数设为1,再将指数从0进行累加,然后人工确认输出结果出现异常的地方,就是指数的最大值。
#include<stdio.h>
int main(){
//求最大正数值的指数部分
double max=1e0;
for(int i=1;i<=400;i++){
max*=1e1;
printf("%d %e\n",i, max);
}
return 0;
}
代码中的循环条件“i<=400”根据实际测试结果给的,这时输出已经出现异常。
根据下图输出结果,可知最大指数为308。
上面的“1.#INF00e+000”是什么东东呢?这其实也是一种科学记数法的表示形式,只不过指部为0,而基数的前半部分“1.#INF”表示“无穷大inf (infinity 的缩写)”,所以“1.#INF00e+000”自然也表示无穷大。但这只是对“无穷大”的数的一种标记方式,它本身并不是一个具体的值,也就不能和其他的值作比较,所以这里很难通过程序实现自动判断数值超限位置。
(2) 求最大正数值的基数部分
上面已经求出最大指数是308,下面就将指数设为308,再将基数从1进行累加,然后人工确认输出结果出现异常的地方,就是基数的最大值。
#include<stdio.h>
int main(){
//求最大正数值的基数部分
double max=1e308;
for(int i=1;i<=1000;i++){
max+=0.01e308;
printf("%d %e\n",i, max);
}
return 0;
}
根据下图输出结果,可知最大基数为1.79,所以double型浮点数最大正数值为1.79e+308,与前面用宏给出的最大值1.79769e+308一致(只不过咱们的精度没有它那么高)。
2. 求最小正数值
(1) 求最小正数值的指数部分
思路是将基数设为1,再将指数从0进行累减,然后人工确认输出结果出现异常的地方,就是指数的最小值。
#include<stdio.h>
int main(){
//求最小正数值的指数
double min=1e0;
for(int i=1;i<=400;i++){
min*=1e-1;
printf("%d %e\n",i, min);
}
return 0;
}
根据下图输出结果,可知最小指数为-324
(2) 求最小正数值的基数部分
上面已经求出最小指数是324,下面就将指数设为324,再将基数从9进行累减,然后人工确认输出结果出现异常的地方,就是基数的最小值。
但是这时候已经不能靠代码进行累减了,因为9e-324本来已经小到接近极限了,没法再让程序减去一个固定的最小值了,所以此时就要人工实现累减。方法也很简单,就是将下面代码中的基数9依次改为8、7、6、5、4、3、2、1后逐一运行程序。
#include<stdio.h>
int main(){
//求最小正数值的基数
double min=9e-324;
printf("%e\n", min);
return 0;
}
输出结果如下:
9e-324输出9.881313e-324
8e-324输出9.881313e-324
7e-324输出4.940656e-324
6e-324输出4.940656e-324
5e-324输出4.940656e-324
4e-324输出4.940656e-324
3e-324输出4.940656e-324
2e-324输出0.000000e+000
1e-324输出0.000000e+000
可见,当改为7e-324以下、3e-324以上时,程序一律将值视为4.940656e-324,当改到2e-324时程序已经将它视为0。
所以double型浮点数最小正数值为4.940656e-324,前面由宏输出的结果为2.22507e-308,咱们得到的值比它这个值还要小很多。
综上,老金这种半自动化的方法虽然不那么先进,但却是三种方法中最简单高效、最接近标准答案的。