当前位置: 首页 > Web前端 > HTML

0.1+0.2不等于0.3吗?原来这个

时间:2023-03-28 14:50:03 HTML

浮点数的精度损失一直是前端面试八股论文中非常普遍的问题。今天我们就来深入了解问题背后的原理,并给出一些日常处理的小技巧。现象:不听话的小数先来看两个现象:第一个现象:0.1+0.2≠0.3第二个现象:2.55.toFixed(1)=2.5,and1.55.toFixed(1)=1.6但是如果你稍微有点经验前端开发,你一定见过第一种现象,第二种现象比较少见,但其实它们的底层原理是相似的,下面就让我们看看这里发生了什么。背景:数学知识为了更好的理解背后的计算原理,我们先来复习一下数学知识:在数学中,小数可以有无限位数,但计算机存储介质有限,不可能全部存储。因此,在计算机领域中所有的小数都只是近似值。科学计数法是一种计数方式,将一个数表示为a乘以n的10次方(1≤|a|<10),缩写:aEn=a*10^n。使用科学记数法可以节省大量浪费的空间和时间。一个数的负n次方等于这个数的n次方的倒数,10^-2=1/(10^2)=1/100。十进制近似:四舍五入,二进制近似:零四舍五入。追根溯源:正整数二进制转换的转换方法:除以二取余,然后倒序排列,高位补零。比如65的转换(65转二进制就是1000001,高位0就是01000001)。负整数的转换方法:将对应的正整数转换成二进制后,将二进制取反,然后将结果加一(这个操作其实是一个方便的操作,其底层原理涉及到补码知识,如果你有兴趣可以阅读文末参考资料)。比如-65先把65转成二进制01000001,逐位取反:10111110加1:10111111(补码)十进制转换方法:小数点后的数乘以2,取整数部分,再取小数部分并乘以2,依此类推...直到小数部分为0或足够的数字。圆形部分可以依次排列。比如123.4:0.4*2=0.8——————>取00.8*2=1.6——————>取10.6*2=1.2——————>取10.2*2=0.4————————>取00.4*2=0.8————————>取0………………则为一个循环依次写:0.4=0.01100110……(0110循环)整数部分123二进制是1111011,所以123.4的二进制表示是:1111011.011001100110...你发现了什么?十进制小数转换为二进制后,大概率会出现无限位数!但是电脑存储空间有限,怎么办?来吧,让我们看看。Traceability:浮点数存储机制浮点数数据类型主要包括:单精度(float)、双精度(double)单精度浮点数(float)在内存中占用4个字节,有8位有效数字,以及表示范围:-3.40E+38~+3.40E+38双精度浮点数(double)占用内存8个字节,有效位16位,表示范围:-1.79E+308~+1.79E+308IEEE754andECMAScriptIEEE754所谓IEEE754标准,全称IEEEBinaryFloating-PointArithmeticStandard,这个标准定义了浮点数的格式等等,类似这样:value=signxexponentxfraction是浮点数的实际值,等于符号位(signbit)乘以指数偏差乘以分数。在IEEE754中,规定了四种表示浮点值的方式:单精度(32位)、双精度(64位)、扩展单精度、扩展双精度。ECMAScript对IEEE754的实现ECMAScript中的Number类型使用IEEE754标准来表示整数和浮点值,使用双精度,即使用64位来存储一个浮点数。在这个标准下,我们将使用1位来存储S(符号),0用于正数,1用于负数。使用11位存储E(exponent)+bias。对于11位,bias的值为2^(11-1)-1,即1023。小数存储在52位中。Forexample,take0.1,thecorrespondingbinaryis11.1001100110011...2^-4,Signis0,E+biasis-4+1023=1019,1019inbinaryis1111111011,Fractionis1001100110011...Corresponding64位的完整表示就是:0011111110111001100110011001100110011001100110011001100110011010同理,0.2表示的完整表示是:0011111111001001100110011001100110011001100110011001100110011010可以看出来在转换为二进制时0.1>>>0.0001100110011001...(1001无限循环)0.2>>>0.0011001100110011...(0011无限循环)将0.1和0.2的二进制形式按实际展开,末尾补零相加,结果如下:0.00011001100110011001100110011001100110011001100110011010+0.00110011001100110011001100110011001100110011001100110100=0.01001100110011001100110011001100110011001100110011001110用科学计数法表示为:1.001100110011001100110011001100110011001100110011010*2^(-2)省略尾数最后的0,即:1.00110011001100110011001100110011001100110011001101*2^(-2)因此0.1+0.2实际存储时的形式是:0011111111010011001100110011001100110011001100110011001100110100再转十进制为:0.30000000000000004好了,奇怪的东西出现了,0.1+0.2isnotequalto0.3!Solvethecase!knockoff.小结在计算机中存储双级数浮点数,需要将十进制转换为二进制科学计数法,然后计算机按照一定的规则(IEEE754)存储,因为有位数限制存储时(double-progress8bytes,64bits)需要对最后一位进行近似处理(0四舍五入为1),转为十进制时会出错。破案!收工。解决方案既然问题已知,那么最好的解决方案是什么?这里有一些想法给你。简单方案纯展示类比如当你从后端获取2.3000000000000001数据进行展示时,可以先使用toPrecision方法保留一定位数的精度,parseFloat(2.3000000000000001.toPrecision(12))之后再展示parseFloat===2.3//true网上有人建议这里默认精度为12,这是一个经验值。一般12位就可以解决大部分0001和0009的问题了。如果需要更高的精度,可以自行调整。运算类对于需要计算的场景(四次算术运算),不能粗暴的使用toPrecision。更好的方法是在进行运算之前将小数转换为整数。我们可以把需要计算的数升级为计算机可以准确识别的整数(乘以10的n次方),计算完成后再降级(除以10的n次方),这是大多数语言处理常用方法的精度问题。0.1+0.2===0.3//假(0.1*10+0.2*10)/10===0.3//真(0.1*100+0.2*100)/100===0.3//真35.41*100===3540.9999999999995//true//即使先放大再缩小,还是会有丢精度的问题(35.41*100*100)/100===3541//falseMath.round(35.41*100)===3541//true看来精度损失的问题不能简单的通过扩缩容来解决。我们可以在浮点数toString之后indexOf("."),记录两个值的小数点后位数的长度,比较,取最大值(也就是展开多少次),然后然后在计算完成后将其缩小。//加法运算函数add(num1,num2){constnum1Digits=(num1.toString().split('.')[1]||'').lengthconstnum2Digits=(num2.toString().split('.')[1]||'').lengthconstmultiplier=10**Math.max(num1Digits,num2Digits)return(num1*multiplier+num2*multiplier)/multiplier}第三方库对数据精度有一定要求对于极高的场景,可以直接使用一些现成的库。这些库本身封装了更复杂的计算方法,相对来说也更准确,比如大数的bignumber.js,小数的number-precision和decimal.js,都是不错的库。类似问题还记得我们第一次展示两种现象的时候吗?上面我们只还原了第一个现象(即0.1+0.2的问题),接下来简单说一下Number.toFixed带来的舍入问题。让我们再看看这个现象:让我们使用toPrecision来保留更高的精度,看看:就是这样!toFixed方法会根据你传入的精度对数字进行四舍五入,2.55其实就是2.54999...如果取1位精度,由于第二位是4,四舍五入后就是2.5。而如果1.55取一位精度,由于第二位是5,四舍五入后就是1.6。那么如何解决此类问题呢?网上给出了一个通用的解决方案。四舍五入前,给数字加上一个最小值,比如1e-14:这样处理之后,大部分场景下的准确率基本够用了。我们在这里使用的最小值是10的负14次方(1e-14)。是否有官方推荐的最小值常数?咦,好巧,还真有!ES6在Number对象中加入了一个非常小的常量Number.EPSILON:Number.EPSILON//2.220446049250313e-16Number.EPSILON.toFixed(20)//"0.00000000000000022204"引入这么小的量,目的是提供浮点数该计算设置了一个误差范围。如果误差可以小于Number.EPSILON,我们就可以认为结果是可靠的。你可以画一个错误检查函数://错误检查函数functionwithinErrorMargin(left,right){returnMath.abs(left-right)