当小数遇上二进制——全面解析JS中的小数二进制计算(附赠0.1+0.2 !== 0.3问题解释)
二进制小数如何转换为十进制
二进制转换十进制的方法是:
- 从二进制数的最低位开始,每一位乘以对应的2的幂数,然后将最终的结果小数部分与整数部分分别相加
- 对应的2的幂,以个位为0,向高位依次增1,向地位依次减1;
举个例子: 以二进制小数1100.0011
为例:
二进制小数位 | 1 | 1 | 0 | 0 | . | 0 | 0 | 1 | 1 |
---|---|---|---|---|---|---|---|---|---|
对应2的幂 | 3 | 2 | 1 | 0 | . | -1 | -2 | -3 | -4 |
乘幂计算 | 1 * 23 | 1 * 22 | 0 * 21 | 0 * 20 | . | 0 * 2-1 | 0 * 2-2 | 1 * 2-3 | 1 * 2-4 |
结果 | 8 | 4 | 0 | 0 | . | 0 | 0 | 0.125 | 0.0625 |
当幂为负数时,乘幂计算实际上就是除以对应幂数的倒数,即1 * 2-3 = 1 / 23 = 1/8 = 0.125;
所以最终的结果就是 8 + 4 . 0.125 + 0.0625 = 12.1875;
我们在浏览器控制台验证一下,看是否正确:
二进制转十进制javascript
实现:
// 这里参数n要直接传入二进制小数的字符串形式,
//如果直接传入一个二进制小数,无论是利用'' +n 还是利用String(n)去转字符串,都会将原值截断,只保留19位,导致最后计算错误
function BinToDec(n) { // let arr = ('' + n).split('.'); 这样会导致计算错误。let arr = n.split('.'); let ZS = arr[0].split('');let XS = arr[1] ? arr[1].split(''): [];let zsSum = 0;let xsSum = 0;let i = 0;let j = 1;while (ZS.length) {let num = parseInt(ZS.pop());zsSum += Math.pow(2, i) * num;i++;}if(XS.length) {while(XS.length) {let num = parseInt(XS.shift());xsSum += Math.pow(2, -j) * num;j++;}}return zsSum + xsSum;
}
十进制小数如何转为二进制小数
十进制小数转换为二进制是整数部分与小数部分分别计算,然后再相加的。
整数十进制转二进制 —— 除2取余法【短除法】
不断除以2,直到商为0,每一步得到的余数依次由低到高填充到二进制的位置里。
因为任何一个十进制除以2的余数要么是1,要么是0,所以最后这些余数就构成了最后的二进制数。
比如将174
这个数字转换为二进制的过程:
除以2 | 商 | 余数 | 二进制第几位 |
---|---|---|---|
174/2 | 87 | 0 | 0 |
87/2 | 43 | 1 | 1 |
43/2 | 21 | 1 | 2 |
21/2 | 10 | 1 | 3 |
10/2 | 5 | 0 | 4 |
5/2 | 2 | 1 | 5 |
2/2 | 1 | 0 | 6 |
1/2 | 0 | 1 | 7 |
所以,得到十进制174转换为二进制为: 10101110
(注意由高位到低位书写,和表格中计算得到的顺序相反)
用javascript
递归实现:
:/*** [integerToBin 整数转二进制]** @param {[type]} n [n description]** @return {[]} [return description]*/function integerToBin(n) {// 如果商大于0,继续递归调用,否则返回空字符串用于与前面的结果连接if (n > 0) {// 获得数字除2后的商let quotient = parseInt( n / 2);let remainder = n % 2;return integerToBin(quotient) + '' + remainder;}return '';}
小数十进制转二进制——乘2取整法
将小数的小数部分取出,乘以2,将得到的结果中的整数部分作为二进制小数的项
得到结果中的小数部分重复第一步的步骤
直到某一步乘以2的值的小数部分为0,或者小数部分形成循环小数则停止
比如
0.1875
这个十进制小数,计算转换为二进制的过程:乘以2 积 整数部分 小数部分 0.1875 * 2 0.375 0 0.375 0.375 * 2 0.75 0 0.75 0.75 * 2 1.5 1 0.5 0.5 * 2 1.0 1 0 得到结果为:
0011
(与整数转换不同,这里是顺序)用
javascript
的递归实现:
/*** [fracToBin 小数转二进制]** @param {[type]} n [n 需要转换为二进制的小数]* @param {[type]} bits [bits 准换后的二进制小数的位数]* @param {undefined[]} bin [arr 转换后的二进制小数,默认为空]** @return {[]} [return description]*/function fracToBin(n, bits = 49, bin = '') {// 如果二进制位小于给定的位数if (bin.length < bits) {// 1. 将需要转换的小数分为整数部分与小数部分,获得小数部分let xs = ('' + n).split('.')[1];// 2. 小数部分乘以2// 2.1小数部分字符串转数字let xsNumber = parseFloat('0.' + xs);// 2.2 小数部分乘以2let a = xsNumber * 2;// 3. 取出数字的整数部分拼接到二进制小数的结果中let ta = ('' + a).split('.');bin += ta[0];let na = parseFloat('0.' + ta[1]);// 如果小数部分等于0,则返回if (parseFloat(na) == 0) {return bin;}// 否则递归return fracToBin(na, bits, bin);}return bin;}
用代码表示十进制转换二进制的整体计算过程:
/*** [DecToBin 十进制转二进制]** @param {[type]} n [n description]** @return {[type]} [return description]*/function DecToBin(n) {let inter, frac;let arr = ('' + n).split('.');inter = arr[0] === '0' ? arr[0] : integerToBin(arr[0]);frac = arr[1] ? '.' + fracToBin(parseFloat('0.' + arr[1])) : '';return parseFloat([inter, frac].join(''));}DecToBin(2.3555);
当然,以上代码仅用于展示十进制小数转换为二进制小数的过程,实际开发中,我们可以直接像下面这样转换:
let a = 2.3555;
console.log(a.toString(2));
二进制小数的存储
浮点数
我们将二进制小数算出来还不算完,还要明白计算机中如何存储二进制小数的。
小数,在计算机语言里,准确应该叫做浮点数。
而浮点数根据精确度不同分为很多种,最常用的有两种:
- 单精度浮点数,采用32位二进制位存储
- 双精度浮点数,采用64位二进制位存储
浮点数精度
所谓精度,就是二进制数能够表达的数的精确度,在计算机中,二进制的存储存在着浮点数精度丢失的风险。
我们来看下小数点后四位能够用二进制所表示的十进制数:
二进制数 | 对应的十进制数 |
---|---|
0.0000 | 0 |
0.0001 | 0.0625 |
0.0010 | 0.125 |
0.0011 | 0.1875 |
0.0100 | 0.25 |
0.0101 | 0.3125 |
0.0110 | 0.375 |
0.0111 | 0.4375 |
0.1000 | 0.5 |
0.1001 | 0.5625 |
0.1010 | 0.625 |
0.1011 | 0.6875 |
0.1100 | 0.75 |
0.1101 | 0.8125 |
0.1110 | 0.875 |
0.1111 | 0.9375 |
这里,左面这一列的二进制数是连续的,它已经穷尽了四位二进制所能表达的所有二进制数
但右边十进制这一列却不是连贯的。
想象一下,我们如何使用四位二进制来展示 0.0715这个数字?
答案是,我们无法用四位二进制来表示,甚至无法用有限的二进制位来表示,按照我们上面介绍过的十进制小数转换为二进制小数的方法,它将得到一个无限小数,然而计算机存储是有上限的,不可能给它无数个位来存储,所以就会将超出最高存储位数上限的部分给截掉,这就是精度丢失的原因。
浮点数存储
根据IEEE 754规范,计算机对浮点数的存储总共分为三部分:
符号位 + 指数位 + 尾数位
每部分的位数根据精度不同而不同,比如
- 32位单精度浮点数的存储格式: 1位符号位 + 8 位指数位 + 23 位尾数位
- 64位双精度浮点数的存储格式: 1位符号位 + 11位指数位 + 52位尾数位
上述三部分我们分别解释下:
符号位,代表数字的正负,为【1】时表示【负数】,为【0】时表示【正数或者0】
尾数位
我们都知道,一个十进制数可以使用多种方式表达,比如
3.14
这个十进制数,他可以表达为以下几种形式:- 314 * 10-2
- 0.314 * 101
- 0.00314 * 103
为了方便计算机处理,科学家们就规定了一个对于十进制数统一的表示规则:小数点前面是0,小数点后面第1位不能是0.
所以所有十进制数有了相同的表达形式:
- 3.14 => 0.314 * 101
- 175 => 0.175 * 103
- 0.003 => 0.3 * 10-2
同样地,二进制小数也可以有多种表达形式,比如
10.10
这个二进制数,可以表达为:10.10
* 201.010
* 21101.0
* 2-1
为了计算机方便处理,计算机科学家规定了对于一个二进制小数的表示规则: 将小数点前面的值固定为1,并且确保小数点后面的长度为规定的精度的尾数位数。
所以所有的二进制小数就有了相同的表达形式(以32位精度为例,其尾数尾数位23位):
- 10.10 =>
1.01000000000000000000000
* 21 - 0.0010 =>
1.00000000000000000000000
* 2-3 - 100.1 =>
1.00100000000000000000000
* 22
而且既然规定了所有数字的整数各位都是1,那么为了节省存储空间,这个1就可以省略了,最后仅保留小数部分,就是这个二进制小数的尾数,以上面三个为例:
- 10.10的尾数: 01000000000000000000000;
- 0.0010的尾数: 00000000000000000000000;
- 100.1 的尾数: 00100000000000000000000;
指数位
指数位采用EXCESS系统表现
EXCESS 系统表现是指,通过将指数部分表示范围的中间值设为 0,使得负数不 需要用符号来表示
拿扑克牌举例,比如我们有从A到K十三张扑克牌,现在我们以中间的 7 为 0,则根据EXCESS系统,形成了以下对应关系:
牌面值 EXCESS系统值 A -6 2 -5 3 -4 4 -3 5 -2 6 -1 7 0 8 1 9 2 10 3 J 4 Q 5 K 6 同样地,当精度为32位时,指数位为8位,它能表示的最大二进制数为
11111111
,即255,我们取它中间的数,即使用
11111111
除以2,得到二进制数01111111
(二进制中,一个数除以2实际上就是右移一位,左面补0),01111111
的十进制数对应的是127,根据EXCESS系统要求,我们将中间值127表示为0, 则最终会形成类似下面的表示
二进制值 十进制值 EXCESS值 …… …… …… 1111100 124 -3 1111101 125 -2 1111110 126 -1 01111111 127 0 10000000 128 1 10000001 129 2 10000010 130 3 …… …… …… 所以上例中的三个数字的对应指数位分别是:(使用小数通用表示规则写出后,指数是2的n次方,这里的n代表EXCESS值,而指数位存储的则是对应的二进制值)
- 10.10的指数位:10000000
- 0.0010的指数位:1111100
- 100.1的指数位: 10000001
所以,上面三个小数
10.10
,0.0010
,100.1
,最终的二进制存储为(按照32位精度):二进制小数 统一表达 正负 指数 符号位 指数位 尾数位 10.10 1.01000000000000000000000
* 21正 1 0 10000000 01000000000000000000000 0.0010 1.00000000000000000000000
* 2-3正 -3 0 1111100 00000000000000000000000 100.1 1.00100000000000000000000
* 22正 2 0 10000001 00100000000000000000000 最终存储的二进制值就是 符号位+指数位+尾数位。
上表是32位精度下的存储,64位精度时的尾数为52位,指数中间值是01111111111 (即十进制的1023)对应EXCESS系统的0。其它以此类推。
二进制知识实战巩固
我们学习了以上知识后,为了检验我们是否掌握,就拿javascript
中最经典的0.1+0.2 !=== 0.3
为例,来分析一下其中的原因。
首先,我们要知道,javascript
存储的二进制数是64位精度的
1.转换
首先,我们分别将0.1
和0.2
转换为二进制小数,可以利用我们上面学过的转换方法,得到的结果是(保留了60位小数):
十进制小数 | 二进制小数 |
---|---|
0.1
|
0.000110011001100110011001100110011001100110011001100110011001
|
0.2
|
0.001100110011001100110011001100110011001100110011001100110011
|
2.改写为统一表达式
我们将二进制小数改写为统一表达式,由于统一表达式要求小数点后的位数要和当前精度(64)位的尾数位数一致(52位),而我们的二进制小数都保留了60位,即使经过改写为统一表达式后左移了几位,但还是多于52位,所以多余部分的我们要截去。
二进制小数 | 统一表达式 |
---|---|
0.000110011001100110011001100110011001100110011001100110011001
|
1.1001100110011001100110011001100110011001100110011001 * 2-4
|
0.001100110011001100110011001100110011001100110011001100110011
|
1.1001100110011001100110011001100110011001100110011001 * 2-3
|
注意:截取的时候不是直接去掉,而是为最大限度保留精度,采取 【0舍1入】的规则
上面两个二进制小数的小数部分第53位都为1,所以舍去的时候要加1,
即上面两个表达式其实不是最终表达式,最终表达式要在尾数部分+1,得到:
0.1
=>1.1001100110011001100110011001100110011001100110011010
* 2-40.2
=>1.1001100110011001100110011001100110011001100110011010
* 2-3
3.获得最终存储值
我们要分别转换为统一表达式后的符号位、指数位、尾数位
这里,尾数可以直接从表达式里拿到,符号位都是0(正数),唯一剩余的就是获取指数位了
在64位精度下,指数位位11位,所以最大二进制值为1111 1111 111
,取中间数,即除以2,右移一位,左位补零,得到0111 1111 111
(十进制1023)
然后根据EXCESS系统规则列出它前后的对应值:
二进制值 | 十进制值 | EXCESS系统值(指数值) |
---|---|---|
011 1111 1011 | 1019 | -4 |
011 1111 1100 | 1020 | -3 |
011 1111 1101 | 1021 | -2 |
011 1111 1110 | 1022 | -1 |
0111 1111 111 | 1023 | 0 |
100 0000 0000 | 1024 | 1 |
100 0000 0001 | 1025 | 2 |
100 0000 0010 | 1026 | 3 |
100 0000 0011 | 1027 | …… |
那么0.1
和 0.2
的指数位分别是EXCESS系统-4
和-3
所对应的值:
0.1
指数位:011 1111 1011
0.2
指数位:011 1111 1100
由此,获得0.1
与0.2
的二进制最终存储值:
十进制值 | 二进制存储值 |
---|---|
0.1
|
0 01111111011 1001100110011001100110011001100110011001100110011010
|
0.2
|
0 01111111100 1001100110011001100110011001100110011001100110011010
|
计算和
现在,我们将上表中两个二进制存储值进行相加,逢二进一,得到二进制存储结果:
0 0111 1111 101 0011001100110011001100110011001100110011001100110100
那么我们如何将它还原回二进制进而还原回十进制小数呢?
逆向工程开始:
- 第1位0,代表正数
- 第2到12位,代表指数,
0111 1111 101
对照上面的EXCESS系统表,是十进制的1021,对应指数位-2
- 第13位到64位,
0011001100110011001100110011001100110011001100110100
代表尾数 - 还原为统一表达式:
1.0011001100110011001100110011001100110011001100110100
* 2-2 - 得到无指数形式:
0.010011001100110011001100110011001100110011001100110100
然后我们利用第一节的二进制小数转换十进制的方法,得到结果:
所以,在javascript
中,0.1 + 0.2 === 0.30000000000000004
会返回true
;
,是十进制的1021,对应指数位-2
- 第13位到64位,
0011001100110011001100110011001100110011001100110100
代表尾数 - 还原为统一表达式:
1.0011001100110011001100110011001100110011001100110100
* 2-2 - 得到无指数形式:
0.010011001100110011001100110011001100110011001100110100
然后我们利用第一节的二进制小数转换十进制的方法,得到结果:
[外链图片转存中…(img-7q6d05kg-1632739132361)]
所以,在javascript
中,0.1 + 0.2 === 0.30000000000000004
会返回true
;
[外链图片转存中…(img-Msu3JYWH-1632739132363)]
当小数遇上二进制——全面解析JS中的小数二进制计算(附赠0.1+0.2 !== 0.3问题解释)相关推荐
- [当人工智能遇上安全] 3.安全领域中的机器学习及机器学习恶意请求识别案例分享
您或许知道,作者后续分享网络安全的文章会越来越少.但如果您想学习人工智能和安全结合的应用,您就有福利了,作者将重新打造一个<当人工智能遇上安全>系列博客,详细介绍人工智能与安全相关的论文. ...
- js中,小数的加减乘除
上图看效果 如果小数直接做运算,很容易发生如上图的精度丢失的情况,这个问题还是百度: javascript 中同理. 为尽量降低这种精度丢失对数据的准确性的影响,做了相关的处理,函数如下: /*** ...
- 十进制有限小数如何以二进制保存而不会变成无限小数
做高精度算法的人可能会遇到一个问题,就是如何表达0.1这个数字.如果把十进制的0.1转换成二进制,就会变成无限的小数.常规的做法是采用十进制高精度算法.但是十进制高精度算法的效率低,浪费内存.而且学过 ...
- 深入解析js中的函数
写在前面 由于词语匮乏,本文继续沿用"深入解析xxx"这个俗套的命名,但是是真的很深入(你要信我啊).如果本文对你有用,欢迎收藏,如果喜欢我的文章,欢迎点赞和关注专栏. 函数可以说 ...
- js中关于带数字类型参数传参丢失首位数字0问题
最近在项目中遇到一个问题,js中传带有数字的参数时,如果参数开头有数字0,会把0给去掉. 例如: 方法abc(0123456,789); 方法abc中获取的参数0123456就会变为123456. 原 ...
- 深度解析机器学习中的置信区间(附代码)
作者:Jason Brownlee 翻译:和中华 校对:丁楠雅 本文约4000字,建议阅读15分钟. 本文介绍了置信区间的概念以及如何计算置信区间和bootstrap置信区间. 机器学习很多时候需要估 ...
- js中获取小数点后两位小数
在 JavaScript 中,可以使用 toFixed() 方法来获取小数点后的指定位数. 例如,如果要获取一个数字的小数点后两位小数,可以这样写: var num = 3.14159265; var ...
- 全面解析js中的for循环
JavaScript诞生已经有20多年了,我们一直使用的用来循环一个数组的方法是这样的: for(var index=0;index<myArray.length;index++) { cons ...
- MATLAB实战系列(十七)-大学生数学建模赛题解析-水塔中水流量估计(附MATLAB源码)
题目 美国某洲的各用水管理机构要求各社区提供以每小时多少加仑计的用水率以及每天总 的用水量,但许多社区并没有测量水流入或流出当地水塔的水量的设备,他们只能代之以每小时测量水塔中的水位,精度在 0.5% ...
最新文章
- 极客新闻——04、WiFi万能钥匙万玉权:管理应该是“自下而上”
- 前端HTML5CSS3动画
- 使用NPOI——C#和WEB API导出到Excel
- ExtJS Panel主要配置列表
- scala学习--难点
- 蜗轮蜗杆计算软件_微型直流电机减速比计算方法
- 10本Java网站开发必看书籍
- qq连连看java版_java仿QQ连连看游戏
- 怎么用计算机拟合函数wps,如何在excel 里利用曲线拟合的方式求公式|
- UDP进程terminated
- Y430P拆机:安装固态硬盘+内存+重装系统梳理
- python证件照_python opencv实现证件照换底的方法
- vue里面使用echarts实现根据浏览器屏幕大小自适应
- 算法第一次作业(2.帐篷问题)
- 本地搭建自己的电影网站,并发布公网访问 1-3
- review设备管理
- 一个移动端开发者,对未来的思考
- 中国量化融业解金工计机计金领就指
- linux amba机制,AMBA仲裁器仲裁机制
- 白羊狮子:爱在烈火中永生
热门文章
- 微信视频点播小程序系统设计与实现
- C语言实现:见缝插针游戏!代码思路+源码分享
- EasyWeChat微信开放平台第三方平台接入流程
- 乐理普及——音乐人常说的Key到底是什么
- Stages—产品开发流程管理解决方案
- Profile_Day05:企业级360全方位用户画像
- Mybatis SelectKey和UseGeneratedKey的区别
- 【剑指Offer(专项突破)】002. 二进制加法(Java实现) 详细解析
- 年月日格式判断-正则表达式 YYYY/MM/DD、YYYY/MM/DD| YY/MM/DD、 ^(^(\d{4}|\d{2})(\-|\/|\.)\d{1,2}\3\d{1,2}$)|(^\d{4}…
- 因为干过外包,我脏了简历!大厂HR透露:干过外包就刷掉