壹 ❀ 引

0.1+0.2不等于0.3,即便你不知道原理,但也应该听闻过这个问题,包括博主本人也曾在面试中被问到过此问题。很遗憾,当时只知道一句精度丢失,但是什么原因造成的精度丢失却不太清楚。而我在查阅资料的过程中发现,大部分文章都是假定了你有一定计算机基础,对于非此专业的人来说,可能文章读起来就显得晦涩难懂。那么本文就会站在此问题的角度,从二进制计算说起说起,用基础数学通俗易懂的去解释究竟是什么原因造成了计算机中浮点数计算的精度丢失,本文开始。

贰 ❀ 从二进制说起

与我们人的计算思维不同,计算运算采用二进制而非十进制,毕竟人可以用十根手指表示十个数字。而对于早期计算机而言,第一代电子管数字机(1946年)在硬件方面,逻辑元件采用的都是真空电子管,而使用电子管表示十种状态过于复杂,所以当时的电子计算机只有两种状态,即开和关,因此电子管的两种状态也奠定了计算机采用二进制来表示数字和数据。

十进制非常好理解,比如一个基础的十进制计算:

9 + 2 = 11 // 逢十进一,剩余1个1,所以等同于10 + 1,因此是11

与十进制的逢十进一不同,二进制只有01两个数字,它遵循逢二进一,比如:

1 + 1 = 10 //1+1等于2,逢二进一,因此为10,这里读作一零,而不是十

除了加法,二进制一样存在加减乘除的操作,比如:

// 加法
0 + 0 = 0; 0 + 1 = 1; 1 + 0 = 1; 1 + 1 = 10;
// 减法
0 - 0 = 0; 1 - 0 = 1; 1 - 1 = 0; 0 - 1 = 1;
// 乘法
0 * 0 = 0; 1 * 0 = 0; 0 * 1 = 0; 1* 1 = 1;
// 除法
0 / 1 = 0; 1 / 1 = 1;

那么到这里,我们了解了二进制的基本计算规则。而回到文章开头的问题,0.1+0.2的操作对于计算机而言,它一定是将十进制的数字转成二进制之后做的计算,所以要想知道精度如何丢失,我们肯定得先知道十进制数字如何转变成二进制,我们接着聊。

叁 ❀ 十进制如何转二进制

十进制数如何转二进制数,我们可以先知晓一个规则,考虑到十进制数字存在浮点数,我们可以总结为:

整数部分除以2,一直算到结果为0为止,逆序取余;小数部分乘以2,一直算到结果为1为止,顺序取整。

什么意思呢?我们来以5.625为例,将其拆分成整数部分5,以及小数部分0.625并分别套用上面的公式:

//整数/2    取余
5 / 2 = 2 1
2 / 2 = 1 0
1 / 2 = 0 1
// 逆序取余(从下往上),因此是101 //小数乘以1 取整
0.625 * 2 = 1.25 1
0.25 * 2 = 0.5 0
0.5 * 2 = 1 1
// 顺序取整(从上往下),因此也是101
// 综合起来,转二进制为 101.101

因此5.625转二进制结果为101.101

OK,我们再来试着转换0.10.2为二进制,先看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.000110011001100110011001100110011001100110011001100...

经过转换我们发现,0.1转二进制会陷入0.2 0.4 0.8 0.6这四个数字的循环,所以最终的结果是一个无限的0.0 0011 0011 0011...的结构。

接着看0.2的二进制转换:

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.0011001100110011001100110011001100110011001100110011001...

好家伙,0.2更直接,直接陷入0.2 0.4 0.8 0.6这四个数字的计算循环,因此它转成二进制也是一个无限的0.0011 0011 0011...类型结构的数字。

叁 ❀ 二进制的指数形式

我们知道,计算机的存储空间一定是有限的,即便数字的占用空间再小,它也没办法存储一个无限大的数,那计算机是怎么做的呢?这里就得引入二进制的指数以及浮点数IEEE 745标准两个概念,我们先说二进制的指数。

十进制的指数很好理解,比如数字1000用指数表示为1 * 10^3,其中10为底数,3为指数,翻译过来就是1 * (10 * 10 * 10),而这个过程其实可以理解成将小数点往左移动了3位;同理,那自然也有也有将小数点往右移,让指数为负数的情况,比如:

1000  1*10^3
0.001 1*10^-3

而二进制的指数与十进制并无区别,只是将指数从10变成了2,一样如果小数点往左移动N位,那么就是2^n,反之往右移动那就是2*-n,看两个简单的例子:

// 这里都是二进制的数字
1010 1.010 * 2^11 // 底数为2,指数为3
0.001 1 * 2^-11 // 底数为2,指数为-3

这里有同学可能就要说了,不是移动3位吗,怎么指数是11,前面已经说了,二进制中只存在数字0和1,数字3转成二进制不就是11了,大家只要心里清楚这里是3即可。

那么说了这么多,指数有什么价值呢?前面也说了计算机内存有限,在有限的空间去尽量描述无限大或者无限小的数字是很有必要的,那么大家可以想想数字10000和数字1*10^4谁更节省空间,以及数字9999999.99999*10^999999在同等空间下,谁能描述更大的数字,很显然指数更胜一筹,那么到这里我们解释了指数的意义以及二进制指数的描述方式。

肆 ❀ 浮点数的IEEE 754标准

在解释完指数,我们了解到指数能描述和存储更大的数字,但即便再大计算机也没办法使用指数后就能存一个无限长的数字,比如上文十进制0.1转成二进制之后的结果。因此有了指数还不够,计算机还是得对数字做取舍,怎么取舍呢?这就得介绍浮点数的IEEE 754标准了,标准如下:

其中符号位占一位,表示这个数字是正数或者负数,如果是正数,符号位是0,反之是负数,那么符号位就是1。

指数部分11位,前面已经解释过指数,比如1*10^11,这里的指数代表的就是11

尾数部分占52位,比如0.11001100,这里的尾数部分指的就是11001100这一部分。

其实说完尾数,大家应该就知道0.1以及0.2转成二进制的无限小数已经得按尾数的占位规则进行取舍了,这里我们再附上转换之后的二进制数:

// 0.1的二进制
0.000110011001100110011001100110011001100110011001100110011001100110011...
// 0.2的二进制
0.00110011001100110011001100110011001100110011001100110011001100110011...

然后我们再将其转换成二进制指数形式:

// 0.1 二进制指数形式,往右移动4位,指数为-4
1.10011001100110011001100110011001100110011001100110011001100110011... // 指数为-4
// 0.2 二进制指数形式,往右移动3位,指数为-3
1.10011001100110011001100110011001100110011001100110011001100110011... // 指数为-3

前文说了,尾数部分只能是52位,因此我们得做取舍,与十进制四舍五入不同,二进制遵循零舍一入的规则,开始转换:

// 0.1 IEE 754
// 53位是1,进一位
1.10011001100110011001100110011001100110011001100110011
// 52位变成了2,逢二进一
1.1001100110011001100110011001100110011001100110011002
// 最终结果
1.1001100110011001100110011001100110011001100110011010 // 指数为-4

上述转换中,因为53位是1,遵循零舍一入,导致52位变成了2,而二进制逢二进一,因此结尾变成了10。

同理我们也对0.2的二进制指数也做尾数取舍:

// 0.1 IEE 754
// 53位也是1
1.10011001100110011001100110011001100110011001100110011
// 零舍一入后再逢二进一
1.1001100110011001100110011001100110011001100110011010 // 指数为-3

转换完成之后我们需要对两个数求和,但因为指数不同不能直接计算,因此我们将0.1的指数也变成-3

// 0.1 指数为-3
0.1100110011001100110011001100110011001100110011001101

由于尾数只能有52位,小数点往右移动了一位,因此我们得再舍弃一位,正好最后一位是0,直接舍弃,所以有了上面的结果。

最后我们对如下两个指数相同的数进行求和:

// 0.1 指数为-3
0.1100110011001100110011001100110011001100110011001101
// 0.2 指数为-3
1.1001100110011001100110011001100110011001100110011010
// 指数为-3的和
10.0110011001100110011001100110011001100110011001100111
// 指数为0的和,尾数只能是52位,再次取舍
0.0100110011001100110011001100110011001100110011001101

求和同样是相同位进行加法计算,遵循逢二进一,这里直接给出结果后,再得出指数为0的结果,由于尾数只能是52位,所以我们再次取舍。

在拿到结果后,我们得将二进制再还原成十进制,转换规则为:

位权展开求和,以小数点为起始位,小数点每往左移动n位,当前位结果为当前数字 * 2^n,小数点往右移动一位,当前位结果为当前数字 * 2^-n

由于上述数字整数部分是0,我们不做考虑,那么最终结果应该为:

0*2^-1 + 1*2^-2 + 0*2^-3 + 0*2^-4 + .... + 1*2^-52

这里我们通过程序来计算这个过程:

const s = '0100110011001100110011001100110011001100110011001101';
let ans = 0;
for (let i = 0; i < s.length; i++) {
ans += (+s[i]) * Math.pow(2, -(i + 1));
};
console.log(ans); // 0.30000000000000004

如上,我们最终转换的结果为0.30000000000000004,这与控制台输出结果完全一致:

那么到这里,我们解释了为什么0.1+0.2不等于0.3,其本质原因是0.1 0.2在转二进制时因为是无限长小数,为符合IEEE 754标准进行长度取舍以及零舍一入所造成的精度丢失。

伍 ❀ 如何判断0.1+0.2等于0.3

我们可以借用Number.EPSILON来做比较,Number.EPSILON表示1与Number可表示的大于1的最小浮点数之间的差值,比如:

console.log( Math.abs(0.1 + 0.2 - 0.3) <= Number.EPSILON);// true

同理,0.1+0.7其实也不等于0.8,我们一样可以用这种方式做对比:

console.log( Math.abs(0.1 + 0.7 - 0.8) <= Number.EPSILON);

陆 ❀ 总

那么到这里,我们解释了0.10.2不等于0.3的本质原因,除此之外,我们也了解二进制与十进制相互转换的规则,以及IEEE 754对于浮点数计算造成的影响。而事实上,并不是所有的浮点数计算都有精度丢失,比如0.5 + 0.5等于1;1/3结果是0.33333...,而当1/3+1/3+1/3时,结果并不是0.999999..而是整数1。当然,在实际开发中当遇到浮点数计算时,我们往往可以将乘以1000或者更大的数之后再进行计算后再还原,尽可能保证其精度的准确性,那么关于0.10.2求和的故事就说到这里了,本文结束。

【js奇妙说】如何跟非计算机从业者解释,为什么浮点数计算0.1+0.2不等于0.3?的更多相关文章

  1. [js]js的惰性声明, js中声明过的变量(预解释),后在不会重新声明了

    js的惰性声明, js中声明过的变量(预解释),后在不会重新声明了 fn(); // 声明+定义 js中声明过一次的变量,之后在不会重新声明了 function fn() { console.log( ...

  2. 1.js基础(以通俗易懂的语言解释JavaScript)

    1.JavaScript组成: ECMAScript: 解释器.翻译 -->几乎没有兼容问题 DOM: Document Object Model -->有一些操作不兼容 BOM: Bro ...

  3. 学以致用:手把手教你撸一个工具库并打包发布,顺便解决JS浮点数计算精度问题

    本文讲解的是怎么实现一个工具库并打包发布到npm给大家使用.本文实现的工具是一个分数计算器,大家考虑如下情况: \[ \sqrt{(((\frac{1}{3}+3.5)*\frac{2}{9}-\fr ...

  4. js浮点数计算问题 + 金额大写转换

    一 js浮点数计算问题解决方案: 1.使用 NumberObject.toFixed(num) 方法 toFixed() 方法可把 Number 四舍五入为指定小数位数的数字. 2.较精度计算浮点数 ...

  5. 关于js浮点数计算精度不准确问题的解决办法

    今天在计算商品价格的时候再次遇到js浮点数计算出现误差的问题,以前就一直碰到这个问题,都是简单的使用tofixed方法进行处理一下,这对于一个程序员来说是及其不严谨的.因此在网上收集了一些处理浮点数精 ...

  6. 为什么js中0.1+0.2不等于0.3,怎样处理使之相等?(转载)

    为什么js中0.1+0.2不等于0.3,怎样处理使之相等? console.log(0.1+0.2===0.3)// true or false?? 在正常的数学逻辑思维中,0.1+0.2=0.3这个 ...

  7. js浮点数计算(加,减)

    最近工作中经常遇到需要处理浮点型计算的问题,开始一直都在用把浮点数先乘以10的对应小数的位数的次方化成整数再去开始计算. 例如100.01+100.02,可以化成(100.01*100+100.02* ...

  8. js浮点数精度丢失问题及如何解决js中浮点数计算不精准

    js中进行数字计算时候,会出现精度误差的问题.先来看一个实例: console.log(0.1+0.2===0.3);//false console.log(0.1+0.1===0.2);//true ...

  9. js for (i=0;i<a.length;a[i++]=0) 中等于0怎么理解?

    js的问题for (i=0;i<a.length;a[i++]=0) 中等于0怎么理解? 很奇怪的一个for循环 竟然是将原来数组的数据全改为0

随机推荐

  1. 专家PID

    前面我们讨论了经典的数字PID控制算法及其常见的改进与补偿算法,基本已经覆盖了无模型和简单模型PID控制经典算法的大部.再接下来的我们将讨论智能PID控制,智能PID控制不同于常规意义下的智能控制,是 ...

  2. Day05 - Flex 实现可伸缩的图片墙 中文指南

    Day05 - Flex 实现可伸缩的图片墙 中文指南 作者:liyuechun 简介:JavaScript30 是 Wes Bos 推出的一个 30 天挑战.项目免费提供了 30 个视频教程.30 ...

  3. java Web开发实现手机拍照上传到服务器

    第一步: 搭环境,基本jdk 1.6+apache tomcat6.0+myeclipse2014 1.我们要清楚自己的jdk版本.因为我们Apache Tomcat配置的成功的前提是版本相对应. 安 ...

  4. python---变量、常量、注释、基本数据类型

    变量 变量:将运算的中间结果暂存到内存中,以便后续程序调用. 变量的命令规则: 变量由字母.数字.下划线组合而成. 不可以数字开头,更不能全是数字. 不能是python的关键字. 不要用中文. 名字要 ...

  5. C语言求最大公约数最小公倍数(多种方法)

    前言 这个求解方式多样化,灵活变动,但是,网上没有很好的资源和很全的代码,特此练习,敲打后,总结成本片文章. 单一求解 一.最大公约数 1.穷举法(最简单求解方式) 利用除法方式用当前的数字不断去除以 ...

  6. float,short类型赋值运算问题

    float f = 3.4; 有错吗? 有错,因为浮点类型默认是double类型,double类型赋值给float类型是大类型赋值给小类型需要进行强转,可在3.4前加(float)进行强转,或者在声明 ...

  7. CuteBot智能小车

    原因 近期,别人送了我一个CuteBot智能小车,拆开一看做工挺精致的,但是这东西是积木图形编程,显然不适合我这个年龄,所以打算给家里的小孩玩. 那么,你可能会问了,为什么要写这篇文章呢?答案当然是用 ...

  8. Go xmas2020 学习笔记 11、io.Reader

    11-Homework #2. 11-Reader. Reader interface. NewReader func. Reader Struct. Len .Size,Read func. Pra ...

  9. clickhouse智能提示编辑器

    对于经常写sql的人来说智能提示是非常重要的,这个非常影响写sql的效率和心情. 这里说的智能提示不仅仅是关键字(select等)的智能提示,还得要做到表字段的智能提示. 例如: 下面是mysql的智 ...

  10. php实验一专属跳转博文

    今天完成了php关于设计个人博客主页的实验一作业. 这是php实验一作业中博客的跳转链接页.