0.28+0.34=? 一个简单小数加法引发的思考

  0.28+0.34=?

  我相信这个简单的加法,谁都会,肯定等于0.62嘛。

  这是两个特别简单的加法,那如果我在其整数位置上加上其他的数字,或者多加几个和项,你是否还能快速算过来?

  我想这时候,我们又得借助计算器了!而这,有时可能就是电脑!尤其是如果咱们借助简单程序语言来算的时候,嘿嘿,可能就不是那么回事了~

不信你看,用javascript算的结果:

用python算的结果:

当然了,我尝试着用其他语言来试一下,结果好像并不都是这样。

其中,java只会在类型转换的时候出现奇怪的值:(当然这在我们写代码时往往很容易这么干)

好了,前言就到此为止!咱们是要来看一下,为什么 1+1不等于2 ?

其实这是由浮点数在计算机中的存储方式决定的,因为计算机只认识0101,所以小数点的保存就需要使用另外的算法来转换了,大概如下:(以下内容参考网络知识库)

计算机中是用有限的连续字节保存浮点数的。 保存这些浮点数当然必须有特定的格式, C/C++中的浮点数类型 float 和 double 采纳了 IEEE 754 标准中所定义的单精度 32 位浮点数和双精度 64 位浮点数的格式。 在 IEEE 标准中,浮点数是将特定长度的连续字节的所有二进制位分割为特定宽度的符号域,指数域和尾数域三个域, 其中保存的值分别用于表示给定二进制浮点数中的符号,指数和尾数。 这样,通过尾数和可以调节的指数(所以称为"浮点")就可以表达给定的数值了。

32位浮点数存储结构如下:

三个主要成分是:

  • Sign(1bit):表示浮点数是正数还是负数。0表示正数,1表示负数
  • Exponent(8bits):指数部分。类似于科学技术法中的M*10^N中的N,只不过这里是以2为底数而不是10。需要注意的是,这部分中是以2^7-1即127,也即01111111代表2^0,转换时需要根据127作偏移调整。
  • Mantissa(23bits):基数部分。浮点数具体数值的实际表示。

根据国际标准IEEE 754,任意一个二进制浮点数V可以表示成下面的形式:
    V = (-1)^s×M×2^E
  (1)(-1)^s表示符号位,当s=0,V为正数;当s=1,V为负数。
  (2)M表示有效数字,大于等于1,小于2,但整数部分的1可以省略。
  (3)2^E表示指数位。

比如:
对于十进制的5.25对应的二进制为:101.01,相当于:1.0101*2^2。所以,S为0,M为1.0101,E为2。
而-5.25=-101.01=-1.0101*2^2.。所以S为1,M为1.0101,E为2。

来看另一篇文章的简单解说(https://www.cnblogs.com/yiyide266/p/7987037.html):

Step 1 改写整数部分
以数值5.2为例。先不考虑指数部分,我们先单纯的将十进制数改写成二进制。
整数部分很简单,5.即101.。

Step 2 改写小数部分
小数部分我们相当于拆成是2^-1一直到2^-N的和。例如:
0.2 = 0.125+0.0625+0.007825+0.00390625即2^-3+2^-4+2^-7+2^-8….,也即.00110011001100110011。

或者换个更傻瓜的方式去解读十进制对二进制小数的改写转换,通常十进制的0.5也(也就是分数1/2),相当于二进制的0.1(同等于分数1/2),

我们可以把十进制的小数部分乘以2,取整数部分作为二进制的一位,剩余小数继续乘以2,直至不存在剩余小数为止。

例如0.2可以转换为:

0.2 x 2 = 0.4     0

0.4 x 2 = 0.8     0

0.8 x 2 = 1.6     1

0.6 x 2 = 1.2     1

0.2 x 2 = 0.4     0

0.4 x 2 = 0.8     0

0.8 x 2 = 1.6     1

.......

即:.0011001.......(它是一个4862的无限循环的二进制数,明白为什么十进制小数转换成二进制小数的时候为什么会出现精度损失的情况了吗)

Step 3 规格化
现在我们已经有了这么一串二进制101.00110011001100110011。然后我们要将它规格化,也叫Normalize。其实原理很简单就是保证小数点前只有一个bit。于是我们就得到了以下表示:1.0100110011001100110011 * 2^2。到此为止我们已经把改写工作完成,接下来就是要把bit填充到三个组成部分中去了。

Step 4 填充
指数部分(Exponent):之前说过需要以127作为偏移量调整。因此2的2次方,指数部分偏移成2+127即129,表示成10000001填入。
整数部分(Mantissa):除了简单的填入外,需要特别解释的地方是1.010011中的整数部分1在填充时被舍去了。因为规格化后的数值整部部分总是为1。那大家可能有疑问了,省略整数部分后岂不是1.010011和0.010011就混淆了么?其实并不会,如果你仔细看下后者:会发现他并不是一个规格化的二进制,可以改写成1.0011 * 2^-2。所以省略小数点前的一个bit不会造成任何两个浮点数的混淆。

好了,看完上面的浮点数的存储原理后,是时候来解答,为什么计算机会算错的问题了!

  1. 遇到小数点后数字转换为实际存储结构时,有的转换是一个死循环,即不可能得到一个精确的值,而这个不精确的值再与其他数据做运算时,得到的结果自然也就可能存在差距了。至于有时候能得到准确的数值,有时候却得不到准备的值,则是和逆转换相关了(即内存结构转换为可视的十进度数据)!

  2. 另一个存在误差的原因,则是因为在计算过程中进行了数据类型的转换,因为原数据本来就不是精确的值,所以在进行类型转换后,就不会得到和原始值直接转化的值的相同结果了。

所以,咱们在做需要高精度的计算场合时,使用计算机语言自带的存储结构可能会不满足咱们的需求,当然这也很容易办到,一般也会有第三方的解决方案,即换一种存储结构就可能能解决这种问题了。

  如 java 中,使用 BigDecimal 来解决需要高精度运算的场景。(BigDecimal的解决方案就是,不使用二进制,而是使用十进制(BigInteger)+小数点位置(scale)来表示小数);BigDecimal应使用string构造更为准确,否则会在第一步转换时出现精度丢失!

最后,附几个加法结果以供参观:

>> 57168.619999999995-11087.28
46081.34
>> 2412.02+11087.64+8338.28+5580.0
27417.940000000002
>> 0.28+0.34
0.6200000000000001
>> 2.28+2.34
4.619999999999999
>> 33.28+3.34
36.620000000000005
>> 3.28+3.34
6.619999999999999
>> 4.28+4.34
8.620000000000001
>> 5.28+5.34
10.620000000000001
>> 8.28+8.34
16.619999999999997
>> 33.28+9.34
42.620000000000005

原文地址:https://www.cnblogs.com/yougewe/p/10015605.html

时间: 2024-11-08 12:18:29

0.28+0.34=? 一个简单小数加法引发的思考的相关文章

一个简单算法题引发的思考<DNA sorting>(about cin/template/new etc)

首先是昨天在北京大学oj网上看到一个简单的算法题目,虽然简单,但是如何完成一段高效.简洁.让人容易看懂的代码对于我这个基础不好,刚刚进入计算机行业的小白来说还是有意义的.而且在写代码的过程中,会发现自己平时学习中不会发现的问题,所以想写下这个博客,主要是便于自己对算法的理解. 来,上题. DNA Sorting Time Limit: 1000MS   Memory Limit: 10000K Total Submissions: 91599   Accepted: 36781 Descript

Windows 上静态编译 Libevent 2.0.10 并实现一个简单 HTTP 服务器

Windows 上静态编译 Libevent 2.0.10 并实现一个简单 HTTP 服务器  大 | 中 | 小  [ 2011-3-30 08:40 | by 张宴 ] [文章作者:张宴 本文版本:v1.0 最后修改:2011.03.30 转载请注明原文链接:http://blog.zyan.cc/libevent_windows/] 本文介绍了如何在 Windows 操作系统中,利用微软 Visual Studio 2005 编译生成 Libevent 2.0.10 静态链接库,并利用 L

Go语言之从0到1实现一个简单的Redis连接池

Go语言之从0到1实现一个简单的Redis连接池 前言 最近学习了一些Go语言开发相关内容,但是苦于手头没有可以练手的项目,学的时候理解不清楚,学过容易忘. 结合之前组内分享时学到的Redis相关知识,以及Redis Protocol文档,就想着自己造个轮子练练手. 这次我把目标放在了Redis client implemented with Go,使用原生Go语言和TCP实现一个简单的Redis连接池和协议解析,以此来让自己入门Go语言,并加深理解和记忆.(这样做直接导致的后果是,最近写JS时

C# 一个简单的秒表引发的窗体卡死问题

一个秒表程序也是我的一个心病,因为一直想写这样的一个东西,但是总往GUI那边想,所以就比较怵,可能是上学的时候学MFC搞出的后遗症吧,不过当我今天想好用Win Form(话说还是第一次写win form)写这么一个东西的时候,居然so easy. 所以说,做不了不可怕,怕的是你不去做,因为你不去做,你就永远不知道你能不能做它.事实证明,大部分你犹豫能不能做的事情,实际上你都能搞定. 虽然成功实现了一个秒表的简单功能,即开始计时和停止.但是却引发了一个关于win form和C#线程的问题. 下面一

一个截取字符串函数引发的思考

背景 前些天,遇到这样一个问题,问题的内容如下: 要求编写一个截取字符串的函数,输入为一个字符串和字节数,输出为按字节截取的字符串.但是要保证汉字不被截半个,如"我ABC", 4,截取后的效果应该为"我AB",输入"我ABC汉DEF", 6,应该输出为"我ABC",而不是"我ABC+汉的半个". 问题 刚看到这个问题的时候,以为还是很简单的,但写出来之后,发现并不是想要的效果.回想一下当时的思路,就发现刚开

Windows 上静态编译 Libevent 2.0.10 并实现一个简单 HTTP 服务器(无数截图)

[文章作者:张宴 本文版本:v1.0 最后修改:2011.03.30 转载请注明原文链接:http://blog.s135.com/libevent_windows/] 本文介绍了如何在 Windows 操作系统中,利用微软 Visual Studio 2005 编译生成 Libevent 2.0.10 静态链接库,并利用 Libevent 静态链接库,实现一个简单的 HTTP Web服务器程序:httpd.exe. 假设 Visual Studio 2005 的安装路径为“D:\Program

Windows 上静态编译 Libevent 2.0.10 并实现一个简单 HTTP 服务器(图文并茂,还有实例下载)

[文章作者:张宴 本文版本:v1.0 最后修改:2011.03.30 转载请注明原文链接:http://blog.s135.com/libevent_windows/] 本文介绍了如何在 Windows 操作系统中,利用微软 Visual Studio 2005 编译生成 Libevent 2.0.10 静态链接库,并利用 Libevent 静态链接库,实现一个简单的 HTTP Web服务器程序:httpd.exe. 假设 Visual Studio 2005 的安装路径为“D:\Program

一个简单的特效引发的大战之移动开发中我为什么放弃jquery mobile

我本想安静的做一个美男子,可是,老板不涨工资,反而,一月不如一月. 我为什么放弃jquery mobile插件选择自己写特效? 在开发中大家都知道效率很重要,一个好的工具可以在开发中大大提升效率,工作做的越多,相应的取得的报酬也就越多,相反就是我自己了. 最近一直在一件事情上,移动线上网站测试必须符合3星,将不合格的网站调优后保证3星,方便线上推广,难免会遇见一些问题,大致为题后期会写一篇随笔总结,“移动开发中网站如何优化”.其中遇见的一个问题就是JS文件过大,CSS文件过大,之前项目一直使用的

大话JS面向对象之扩展篇 面向对象与面向过程之间的博弈论(OO Vs 过程)------(一个简单的实例引发的沉思)

一,总体概要 1,笔者浅谈 我是从学习Java编程开始接触OOP(面向对象编程),刚开始使用Java编写程序的时候感觉很别扭(面向对象式编程因为引入了类.对象.实例等概念,非常贴合人类对于世间万物的认知方式和思考方式.对于复杂的事物,人类是如何去认识.归纳.总结的?面向对象式编程就是在努力回答这个问题,而答案的核心就是两个字:抽象.所以面向对象式编程特别适合处理业务逻辑,因此被广泛应用于目前的软件开发当中.因为我们开发软件就是为了解决问题,面向对象式编程符合人类对于“问题”的认知方式),因为我早