查看原文
其他

JS番外:解决JS浮点数精度问题

叉烧 CS的陋室 2022-08-08

经谢师兄启发,最近在进行JS浮点数计算精度的问题,算是有了些进展,下面是我的笔记。(Markdown编辑),代码看着难受可以直接上我这个网址上看:


http://note.youdao.com/noteshare?id=db00952021368923eb3e6aa237fbec09


来首音乐呗

正文开始!

问题来源

关于这个问题,最常见,大家讨论的最火的一个热点是0.1+0.2的问题,运行代码就能发现猫腻。

var num1=0.1;var num2=0.2;var res=0.1+0.2; alert(res);  // 0.30000000004

问题原因

答案来源于百度,链接js上0.1+0.2为什么不等于0.3(我的链接因为权限原因暂时起不了作用)

下面的内容我进行精简和修改后,大家好好看。

js中的数字都是用浮点数表示的,并规定使用IEEE 754 标准的双精度浮点数表示。IEEE双精度格式具有53 位有效数字精度(包含符号号),并总共占用64 位。

整数的二进制转化相信大家已经颇为熟悉,其实就是让十进制数不停地除以2,不停地记录余数可得,而小数的转化稍微展开,举一个例子说说:

235.725的小数部分取出,即:0.725,将其乘以进制数二进制就乘以2后得到1。45,取其整数部分1为二进制小数的第一项(十分位),在将小数部分0。45乘2得0。9,取其整数部分为二进制小数的第二位(百分位)0,在将其小数部分0。9乘2,得1。8,取其整数部分为二进制小数的第三位(千分位)1,取其小数部分0。8再乘2……以此类推,直到值为0或形成循环小数则停止。

看完这个例子,继续看这个问题的分析:

根据计算,0.1转化为二进制是一个无限小数,0.00011001100110011…(循环0011),算上符号号,就只有53位有效数字,所以只能够存储52个数字,即0.00011001100110011001100110011001100110011001100110011001。

0.2转化为二进制也是一个无限小数,0.0011001100110011…(循环0011),同样存储52个数字的结果是0.00110011001100110011001100110011001100110011001100110011。

两者求和是0.01001100110011001100110011001100110011001100110011001100,化回十进制就是0.30000000000000004。

所以核心问题就是JS转化数字为二进制计算时用的不是实际数而是近似数,所以会带来近似问题。

解决方案

该方法的核心思路是在计算的时候尽可能避免用浮点数计算。一般而言有两个解决思路,一个是将整数部分和小数部分分开,整数部分直接按照整数计算,小数部分也计算,经过整理后在此整合成完整的数,另一个是两个数同时乘一个系数使之都变为整数之后进行计算,然后再除以该系数。

思路1

整数部分和小数部分分别计算

// 执行部分    var num1=0.01;    var num2=0.2;    alert(num1+num2);    alert(Add(num1,num2));    // 将小数分段    function splitdecimal(num){        // 0:整数部分,1:小数部分,2:小数位数        var numsplit=num.toString().split(".");        numsplit[numsplit.length]=numsplit[numsplit.length-1].length;        return numsplit;    }    // 执行加法    function Add(num_1,num_2){        var num1=splitdecimal(num_1);        var num2=splitdecimal(num_2);        var res=[];// 结果0:整数部分,1:小数部分,2:小数位数        var zero=""; // 小数点后0的个数        // 非零部分转为浮点数        num1[0]=parseFloat(num1[0]);        num1[1]=parseFloat(num1[1]);        num1[2]=parseFloat(num1[2]);        num2[0]=parseFloat(num2[0]);        num2[1]=parseFloat(num2[1]);        num2[2]=parseFloat(num2[2]);        //整数部分求和        res[0]=num1[0]+num2[0];        // 非零小数部分求和        var temp=Math.abs(num2[2]-num1[2]);        if(num1[2]<num2[2]){            num1[1]=num1[1]*Math.pow(10,temp);        }else{            num2[1]=num2[1]*Math.pow(10,temp);        }        res[1]=num1[1]+num2[1];        // 是否有进位        res[2]=Math.max(num2[2],num1[2]);        if(res[1]>res[2]){            var temp=res[1]%Math.pow(10,res[2]);            res[0]+=Math.floor(res[1]/Math.pow(10,res[2]));            res[1]=temp;        }                // 小数部分是否为0        if(res[1]==NaN || res[1]==0){            res[1]="";        }else{            // 移动小数点            var i=1;            var temp=res[1];            while(temp<Math.pow(10,res[2]-1)){                zero+="0";                temp*=10;            }            res[1]="."+zero+res[1].toString();        }        result=res[0].toString()+res[1];        return result;    }

思路2

乘以特定系数都变为整数再进行计算,计算后除回去

// 执行部分    var num1=1.01;    var num2=0.2;    alert(num1+num2);    alert(Add(num1,num2));    // 加函数    function Add(num1,num2){        // beta:小数位数,k:系数        var num1float=num1;        var num2float=num2;        var num1beta=numbeta(num1float);        var num2beta=numbeta(num2float);        var beta=Math.max(num1beta,num2beta);        var result=0;        var k=Math.pow(10,beta);        // alert(num1float*beta);        result=(num1float*k+num2float*k)/k;        return result;    }    // 求小数位数    function numbeta(num1){        var res=num1.toString().split(".")[1].length;        return res;    }


我只写了加法,大家可以尝试其他运算,另外github上有一个项目就是专门解决这个问题的,写的厉害的不行,Big.js,大家可以去了解一下!


在结束之前给大家推荐一个公众号,这是我一个高中同学最近刚开的公众号,他刚毕业,签到思特沃克中国工作,热爱分享,和大家分享!这个公众号主要发布一写成熟的技术或者是新技术的文章和思考,有时也会说说程序员的心里话,大家可以关注一下~鼓掌


那些年我们码过的程序

微信号:zzy_bing


没完!当然还有我的,扫下面的二维码,关注我!谢谢大家!

微信:zgr950123


QQ:545281848
欢迎关注


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存