整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

javascript分数加减表达式

一题计算分数加减表达式的值。加减表达式是1/1、1/2、1/3、1/4、1/5这种多分之一,又加又减的状态,其实还是一个累加的变种,答案是要保留四位小数。

上一题是按照要求用整数去计算的,这个就投机取巧一下,直接给它写成一个1.0/1、1.0/2的形式,先加后减,直接写就行了。这是计算分数加减表达式的值,这些也就都不要了。

需要从1/1到n分之一,所以four循环,int i等于1,i小于等于n,i加用来表示1.0除以这个分数,然后用ws等于0,累加初始化s加等于它。这样一来就能保证这个数字是1/1加1/2加1/3加1/4的状态,求的是1/1减1/2加1/3减1/4,求的是这个。

所以得知道它的前面是正号还是负号,假设它是正号,那么就可以让它等于1S,每次加的时候用1乘以它,符号还是正号,计算一次,用它乘以-1就变成负数了,负数再乘以-1就变成正数了,再乘以-1又变成负数了,以此就可以加入加减的运算。

最后输出s,保留小数,应该是4位,如果没记错,四位提交。今天这个通过还算很顺利。

4位浮点型数据运算

在这周网络授课讲解变量与常量案例章节中,在计算0.1+0.2时,出现的结果与预期不符合,对此问题很多学生较为纠结。为解决这个问题,本节课主要讲授JavaScript数值型数据存储格式及浮点型数据的加运算操作,以解决各位同学的困惑。


问题引入?

例题:请编写JS程序计算0.1+0.2,并通过控制台输出计算结果。

浮点数计算问题

该程序直观判断可知计算结果为0.3,但是通过控制台输出显示的结果却不是0.3,其计算结果为:0.30000000000000004。这一问题也是在浮点类型数据相加操作中代表性问题之一。产生这一问题的原因在于浮点类型数据的存储与运算规则、过程。以下对问题进行解释说明。

浮点数加操作问题

JavaScript数值存储

JavaScript程序设计语言存储数值型数据与其他程序设计语言相比较,没有单独对整数、小数进行精确划分,在实际存储过程中将所有数值按照IEEE754标准使用64位浮点数存储数值。64位浮点类型也称为双精度浮点类型,占用8个字节,共64位。存储结构如下图所示:

64位浮点类型数据结构

在该结构中,64位共分为三组,最高位为符号位用于表示数值的正负,即上图蓝色部分,绿色部分11位用于存储指数值(浮点类型表示类似于科学计数法),剩余52位用于存储小数位即有效数字。例如:0.1 的64位浮点表示值为:

0011111110111001100110011001100110011001100110011001100110011010

10进制小数转换为64位浮点类型

本例所提出的问题需要计算0.1与0.2的和,针对JavaScript脚本语言首先需要将其转换为64位浮点类型。浮点类型数据的表现形式及说明如下:

浮点类型数值形式

其中s符号位占1位,指数E占11位,有效数字占52位。其M值可通过左移右移实现在1~2范围之内。如计算M值为101*2^2可表示为1.01*2^4。对于各部分IEEE754给出了明确的定义,其定义描述如下:

64位浮点数描述

在执行过程中首先需要将十进制小数转换为2进制表示形式,针对题目操作数0.1余0.2,他们对应二进制表示形式如下:

0.2十进制:0.00110011(下划线部分表示无限循环)
0.1十进制: 0.000110011(下划线部分表示无限循环)

两位小数的二进制表示描述如上,如实际存储过程中需要指定长度的话直接用循环部分填充即可。以计算出的二进制为基础可以求出对应的s、M、E分别为多少,其中10进制的0.2求解结果如下:

64位浮点存储求解

以上为基础我们可以求出两位操作数的64位存储相关参数。求解结果描述如下图所示:

64位浮点表示数据

浮点类型数据的加法操作

64位浮点类型数据在进行加法运算时需要按照其运算规则执行运算,其运算规则描述如下:

浮点数的加法操作过程


1.对阶

主要是指将两个操作数的指数部分调整为一样,本例题指数部分分别为-3,-4,按照对阶要求向大的靠拢,需要将-4指数调整为-3。在对阶过程需要对M部分作出对应调整。调整结果描述如下:

对阶处理

经过对阶处理之后所有的指数都变为01111111100。因此我们可以进一步对M部分进行加法操作。

2.M尾数运算

尾数运算部分主要按照二进制数值进行求和运算即可。在计算过程中可能因为进位关系导致数据整体长度发生变化即产生溢出。本例尾数部分运算过程与结果描述如下:

尾数求和

3.规格化处理(右规)

本例运算过程由于加运算产生进位而导致溢出,最终计算结果形式为10.x x x … x。这种形式需要通过对其右规实现规格化处理。尾数每右移一位,阶码相应加 1,最高位补0。

规格化处理

4.舍入处理

在规格化过程中随着右移操作,最右侧位将会被丢掉,因此需要通过舍入处理减少因丢弃导致的精度损失。本例题采用就近舍入方法,最后舍弃位为1,舍弃之后在剩余结果最低位加1,因此最后四位变为0100。操作结果描述如下:

舍弃处理操作

综上所述,本例题最终0.1加0.2的计算结果二进制表示为:

01.0011001100110011001100110011001100110011001100110100*2^-2

问题解决

在获取二进制计算结果之后,为方便观察,我们将该二进制数据恢复为10进制数据,然后进行结果判断,首先去掉指数部分结果如下:

0.010011001100110011001100110011001100110011001100110100

转换结果为:

0.30000000000000004441

如上所示,通过实际手工计算浮点型数据加运算,我们可以清楚了解在JavaScript进行0.1与0.2加运算时出现0.30000000000000004的原因了。如有问题不清楚同学可在评论区留言评论,如发现错误也可留言,大家共同探讨、学习、提高。



本头条号长期关注编程资讯分享;编程课程、素材、代码分享及编程培训。如果您对以上方面有兴趣或代码错误、建议与意见,可以联系作者,共同探讨。更多程序设计相关教程及实例分享,期待大家关注与阅读!系列教程链接如下:

JavaScript基础教程(二)变量、常量与运算符

JavaScript基础教程(一)课程说明

S 加法器模拟,实现

  • 半加器
  • 全加器
  • 波纹进位加法器
  • 全部代码
  • 补码 & 减法

常规位运算

位运算 & 简单的 assert 断言

// 常规位运算
// https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Bitwise_Operators
const AND = (a, b) => a & b;
const OR = (a, b) => a | b;
const XOR = (a, b) => a ^ b;
const NOT = a => ~a;
// Fake Node Assert Lib
const assert = {
 deepEqual: (a, b) => {
 if (a.toString() === b.toString()) return;
 throw new Error(`Not Equal: ${a} ${b}`);
 }
}

半加器

/**
 * 半加器
 * 两个 bit 输入,输出数组 [进位,和]
 * 如:
 * 1,1 => [1, 0]
 * @param {bit} a 输入
 * @param {bit} b 输入
 */
const HalfAdder = (a, b) => [a & b, a ^ b];
// 半加器测试
assert.deepEqual(HalfAdder(0, 0), [0, 0]);
assert.deepEqual(HalfAdder(0, 1), [0, 1]);
assert.deepEqual(HalfAdder(1, 0), [0, 1]);
assert.deepEqual(HalfAdder(1, 1), [1, 0]);

全加器

/**
 * 全加器
 * 两个 bit 输入,和进位输入,输出数组 [进位,和]
 * 如:
 * 0,1,1 => [1, 0]
 * @param {bit} a 输入
 * @param {bit} b 输入
 * @param {bit} c 进位输入
 */
const FullAdder = (a, b, c) => {
 var t1 = HalfAdder(a, b);
 var t2 = HalfAdder(t1[1], c);
 return [t1[0] | t2[0], t2[1]];
}
// 全加器测试
assert.deepEqual(FullAdder(0, 0, 0), [0, 0]);
assert.deepEqual(FullAdder(1, 0, 0), [0, 1]);
assert.deepEqual(FullAdder(0, 1, 0), [0, 1]);
assert.deepEqual(FullAdder(0, 0, 1), [0, 1]);
assert.deepEqual(FullAdder(1, 1, 0), [1, 0]);
assert.deepEqual(FullAdder(1, 0, 1), [1, 0]);
assert.deepEqual(FullAdder(0, 1, 1), [1, 0]);
assert.deepEqual(FullAdder(1, 1, 1), [1, 1]);

波纹进位加法器


/**
 * 波纹加法器, 4 位加法器
 * 如:
 * [0, 1, 0, 1],[0, 1, 0, 1] => [1, 0, 1, 0]
 * @param {Array<Number>} a 4位 bit 输入数组,如:[0, 1, 0, 1]
 * @param {Array<Number>} b 4位 bit 输入数组,如:[0, 1, 0, 1]
 * @returns {Array<Number>}
 */
const RippleCarryAdder = (a, b) => {
 let carry = 0;
 let bit = 3;
 let result = [];
 
 while(bit >= 0) {
 let temp = FullAdder(a[bit], b[bit], carry);
 carry = temp[0];
 result.push(temp[1]);
 bit--;
 }
 return result.reverse();
}
/**
 * 将数字转成 4 位二进制数组
 * 如:
 * 1 => [0, 0, 0, 1]
 * 3 => [0, 0, 1, 1]
 * @param {Number} a 数字
 * @returns {Array<Number>}
 */
const to4Bit = a => (
 a.toString(2)
 .split('')
 .reverse()
 .concat(Array(4).fill('0'))
 .slice(0,4)
 .reverse()
 .map(i => +i)
);
/**
 * 将二进制字符串转为数字
 * 如:
 * '1010' => 10
 * @param {String} a 4 位二进制字符串
 * @returns {Number}
 */
const from4Bit = a => parseInt(a, 2);
/**
 * 加法简写工具
 * @param {Number} a 输入
 * @param {Number} b 输入
 */
const helper = (a, b) => (
 from4Bit(RippleCarryAdder(
 to4Bit(a),
 to4Bit(b)
 ).join(''))
)
assert.deepEqual(helper(0, 0), 0);
assert.deepEqual(helper(1, 1), 2);
assert.deepEqual(helper(1, 2), 3);
assert.deepEqual(helper(2, 2), 4);
assert.deepEqual(helper(3, 5), 8);
assert.deepEqual(helper(1, 14), 15);
// 9 + 14 为 23,但由于我们写的是 4 位加法器,所以有溢出
// 最终的结果需要 mod 0x10(也就是 16)
assert.deepEqual(helper(9, 14), 23 % 0x10);
assert.deepEqual(helper(9, 14), 7);

关于补码 & 减法

我们以 4 bit 存储数字,并以最高位作为符号位

  • 2 - 1 = 2 + (- 1) = 0010(补)+ 1111(补)= 0010(补)= 17 % 16 = 1
  • 2 - 3 = 2 + (- 3) = 0010(补)+ 1101(补)= 1111(补)= 15 % 16 = -1