一道OJ引发的关于C语言浮点数类型数值比较问题的思考
前言
帮助别人的过程也是一种学习,最近在为小伙伴解决问题的时候,遇到了一个我觉得需要引起重视的问题,这个point也许在大家的学习过程中会被提及到,但是要引起重视还是需要一些详细的解释吧。希望大家共勉!
提示:以下是本篇文章正文内容,下面案例可供参考
一、问题来源
1.网站链接
武汉科技大学OJ平台-contest-练习4-1010
链接: WUST Online Judge.
2.题目描述
- Description
按照规定,在高速公路上行使的机动车,超出本车道限速的10%则处200元罚款;若超出50%,就要吊销驾驶证。
请编写程序根据车速和限速自动判别对该机动车的处理。
- Output
每组测试数据的输出在一行中输出处理意见:
(1)若属于正常行驶,则输出“OK”;
(2)若应处罚款,则输出“Exceed x%. Ticket 200”;
(3)若应吊销驾驶证,则输出“Exceed x%. License Revoked”。
其中x是超速的百分比,精确到整数。
- Sample Input
65 60
110 100
200 120
- Sample Output
OK
Exceed 10%. Ticket 200
Exceed 67%. License Revoked
二、问题前导
1.题目分析
- 算法(示例):
分析题目发现,这道题目的算法其实是很简单的顺序执行加选择判断,不过就是加上了多组数据输入的循环输入模块,基于提问同学所给出的代码,忽略掉效率问题,我大致整理出了可以AC的方案。#include<stdio.h> int main() { int v,max_v;//当前速度 最大限速 float delta,d; while(scanf("%d %d",&v,&max_v)!=EOF && v>0 && max_v>0)//多组数据输入模板 { delta=v-max_v;//当前速度与最大速度差值 d=delta*100/max_v;//计算百分比,并取分子便于比较运算 if(d<10) printf("OK\n");//超速低于最大限速10%的不予追究 else if(d<50) printf("Exceed %.0f%%. Ticket 200\n",d);//超速在10%~50%之间的罚款 else printf("Exceed %.0f%%. License Revoked\n",d);//超速50%以上回炉重造 } return 0; }
- 输出结果
输入样例:110 100 200 120 .... 输出 :Exceed 10%. Ticket 200 Exceed 67%. License Revoked ....
2.意外的收获
意外的收获总会发生在对同一件事的研究之上。还是这个问题,有一位小粗心悄咪咪地修改了题目条件,把“输入两个正整数”理解成了“输入两个正数”去处理问题,产生了新的问题。这里给出他的代码:
#include<stdio.h>
int main()
{
double v,max_v;//当前速度 最大限速
//double border;
while(scanf("%lf %lf",&v,&max_v)!=EOF)//多组数据输入模板
{
if(v<max_v*1.1) printf("OK\n");//超速低于最大限速10%的不予追究
else if(v<max_v*1.5) printf("Exceed %.0f%%. Ticket 200\n",(v-max_v)*100/max_v);//超速在10%~50%之间的罚款
else printf("Exceed %.0f%%. License Revoked\n",(v-max_v)*100/max_v);//超速50%以上回炉重造
}
return 0;
}
可以看到这里与能给出正解的代码的区别
/*变量声明为double,输入接受相应的用%lf代替*/
double v,max_v;//当前速度 最大限速
/*条件判断形式如下*/
v<max_v*1.1
这样会造成怎样的结果呢
输入样例为110 100
OK
通过输出(v-max_v*1.1)的值(保留三位小数)我们可以发现,结果显示的是-0.000
明明两个相同的结果为什么会减出负值呢
可以看到,因为两句话的区别,代码的正确性大打折扣,可以看到,按照操作者的理解,输入110 100时,应当输出超速10%,即v<max_v*1.1是不成立的,程序不应该执行里面的OK,但本例恰恰就输出了OK,显然就是这句话出了问题。至于怎么去理解为什么两个人脑认为应该不成立的条件反而成立了这一问题,这就要去考虑浮点数类型float、double在存储方面的内容了。
2.关于float、double型数据在比大小时的问题(你要答案在这里)
- 基本信息
float 数据类型被认为是单精度。double 数据类型通常是 float 的两倍大小,因此被认为是双精度。这些数据类型的确切大小取决于当前使用的计算机。double 至少与 float 一样大。以下是二者在机器中占有字节数、有效数位表示等的对比。
float | double |
---|---|
4字节 | 8字节 |
有效数位7位 | 有效数位16位 |
单精度 | 双精度 |
----浮点数内部的存储方式和科学计数法是一样的。以数字 47 281.97 为例,在科学计数法中,这个数字是 4.728
197X104。其中,104 等于 10000,4.728 197X 10 000 也就是 47 281.97。该数字的第一部分,即
4.728 197,称为尾数。
----计算机通常使用 E 符号来表示浮点值。以 E 符号表示,数字 47 281.97 就应该是 4.728 197E4。E 前的数字部分是尾数,E 之后的部分是 10 的幂。当内存中存储一个浮点数时,它将被存储为尾数和 10 的幂,如表 1 所示。
C++ double和float(浮点类型)详解
- 用例解释
众所周知,计算机存储数据是以二进制的形式存储的,而我们在写代码的时候,输入十进制的数目需要转化为2进制,无论是上面的4.728,还是开篇题目里面的1.1都需要转化,那么读者可以尝试一下将1.1转化为二进制是怎样的一个过程,笔者给出1.1小数部分的转化过程
0.1转化成二进制的算法:
0.1*2=0.2-------取出整数部分0
0.2*2=0.4-------取出整数部分0
0.4*2=0.8-------取出整数部分0
0.8*2=1.6-------取出整数部分1
0.6*2=1.2-------取出整数部分1
0.2*2=0.4-------取出整数部分0
0.4*2=0.8-------取出整数部分0
0.8*2=1.6-------取出整数部分1
0.6*2=1.2-------取出整数部分1
接下来会无限循环
0.2*2=0.4-------取出整数部分0
0.4*2=0.8-------取出整数部分0
0.8*2=1.6-------取出整数部分1
0.6*2=1.2-------取出整数部分1
所以0.1转化成二进制是:0.0 0011 0011 ......
从转化过程我们可以看到,即便是简单的一个0.1在转化成二进制的过程中也会很麻烦,且生成的是一个无限循环值,显然计算机是不会也不可能分配这么多空间去存储一个0.1的,在存储长度固定的情况下,计算机会选择损失一定的数据精度,取得的只是一个估计值,存储到对应的浮点数类型里,上例显示的-0.000也可以用这个来解释。能够用于存储字节越多,存储值才会越接近真实十进制值,所以并不是和我们计算的结果一样,一般会有微小的偏差,但这并不代表这些偏差是忽略不计的,尤其是在大小比较的时候,需要采取特定的手段。
试着将错误示例中的
v<max_v*1.1修改为fabs(v-max_v*1.1)<0便可以解决这一问题
当然也有很多其他的方法,大家可以参考其他资料
多多举一反三
记得引入头文件 <math.h>
相关博客:来源博主:博文豆芽菜
float和double精度误差的问题总结(精确计算)
上一篇: 正确使用指针跨作用域的方法
下一篇: js没有块级作用域但有函数作用域