壹 ❀ 引

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. 顺利通过EMC实验(15)

  2. Azure DevOps 中 Dapr项目自动部署流程实践

    注:本文中主要讨论 .NET6.0项目在 k8s 中运行的 Dapr 的持续集成流程, 但实际上不是Dapr的项目部署到K8s也是相同流程,只是k8s的yaml配置文件有所不同 流程选择 基于 Dap ...

  3. A小程序与B小程序相互跳转的一点记录

    要点速览: A小程序和B小程序关联同一个公众号 B程序的用户授权 A小程序和B小程序的用户关联 诸葛 io 统计用户访问信息 需求:微信放开小程序互跳的 API 后,一些导流和拉新等活动可以在新的小程 ...

  4. java1.7之后的比较器特别之处

    在jdk1.7环境下使用Collectons.sort()方法: 比如:Collections.sort(list, new Comparator<Integer>()); 就可能会出现异 ...

  5. 数据库查询中where和having的用法

    1.类型: "baiWhere"是一个约束声明,在查询数据库du的结果返回之前对数据库中zhi的查询条件进行约束dao,即在结果返回之前起作用,且where后面不能使用" ...

  6. python---快速排序的实现

    def quick_sort(alist, start, end): """快速排序""" # 递归退出 if start >= en ...

  7. 微信小程序一些标签

    wxml标签   一.视图容器(View Container): 二.基础内容(Basic Content) 标签名 说明 标签名 说明 view 视图容器 icon  图标 scroll-view ...

  8. Codeforces Round #720 (Div. 2) B. Nastia and a Good Array(被坑好几次)1300

    原题链接 Problem - B - Codeforces 题意 给一串数,要把任意两个相邻的数的最大公约数=1 每次可以进行一个操作: 取下标为i, j的数,和任意二数x,y,且min(ai,aj) ...

  9. go源码阅读 - sync/mutex

    Mutex是go标准库中的互斥锁,用于处理并发场景下共享资源的访问冲突问题. 1. Mutex定义: // A Mutex is a mutual exclusion lock. // The zer ...

  10. HCIE笔记-第十节-静态路由

    协议 :标识 前方的目的网络 是通过什么协议形成的 优先级:代表形成路由的协议的优先级数值 [厂商规定] 开销值:代表该路由协议形成此路由时的开销 -- 不同的协议计算开销值的方式有区别(越小越优) ...