数学计算之JS小数精度问题(java/python)

number 小数计算会出现精度不准确问题,js中number是64 位双精度浮点数。

其实,不仅仅只有javascript,还有java、python等都会有类似问题,因为计算机中存的都是二进制(IEEE754二进制浮点数算术标准是被普遍使用的标准),小数转二进制时是乘2取整,再用余下的小数部分乘2再取整,如此反复。有的小数转二进制可能会无限循环比如0.1、0.2等,计算机中不可能提供无限个bit位去存储它们,会舍弃一部分二进制位,因而造成了精度损失(即IEEE754尾数(给出了有效数字的位数)的位数有限,需要进行舍入(舍弃多余的位数) 常用方法有0舍1入、恒置1等)

0.1+0.2=0.30000000000000004

0.1 + 0.2 === 0.3 //为false

计算机中常用的数字数据表示格式有两种,一是定点格式,二是浮点格式。

一般来说,定点格式容许的数值范围有限,要求的处理硬件比较简单。而浮点格式容许的数值范围很大,要求的处理硬件比较复杂。

(1)定点格式:即约定机器中所有数据的小数点位置固定不变,小数点就不再使用 “ . ” 表示,隐含小数点。

分为定点整数(带符号整数): 1、11、111等 和 定点小数:0.1、0.11、0.111等。

(2)浮点格式(浮点表示法):把数的范围和精度分别表示的方法,相当于数的小数点位置随比例因子的不同而在一定的范围内可以自由浮动。

指数(阶码)部分: 阶码通常是用补码、移码表示的定点整数

阶符用0表示正指数,用1表示负指数

阶码的数值部分指明了小数点在数据中的位置,因而决定了浮点数的数量级(数值大小)

尾数部分: 尾数通常是用补码、原码表示的定点小数

数符:用0表示正数,用1表示负数

尾数的数值部分:给出了有效数字的位数,因而决定了浮点数的表示精度

(3)浮点数尾数的规格化:

如果不进行规格化,同一个浮点数的表示就不是唯一的。

要求尾数的最高数值位必须是一个有效值(类比十进制科学计数法,通常我们会让数值部分最高位为非0)

原码规格化:不论正数还是负数,第一数位为1

补码规格化:符号位和第一数位不同

(4)IEEE754标准:

比如:FLOAT单精度浮点型占用4个字节(32位),可以存储大约7位有效数字.

FLOAT单精度浮点型使用1位作为符号位(表示正负号),8位作为指数(阶码)部分和23位作为尾数部分。其中,指数部分用于表示浮点数的数量级,尾数部分用于表示浮点数的精度。由于指数部分占用8位,所以可以表示2^8 = 256个不同的指数值(-128~127) 阶码全1、全0用作特殊用途,故真值正常范围:-126~127。

根据IEEE 754标准的规定,FLOAT类型的有效位数约为23位。这意味着尾数部分最多可以表示23位的二进制小数(由于浮点数的规范化表示,尾数部分默认情况下隐藏了最高位的1)。将这个二进制小数转换为十进制,2^23=8388608大约可以表示7位有效数字。

不论是32位浮点数还是64位浮点数,如果不对浮点数的表示作出明确规定,同一个浮点数的表示就不是唯一的。

为了提高数据的表示精度,当尾数的值不为0时,尾数域的最高有效位应为1,这称为浮点数的规格化表示,尾数部分会隐藏表示最高位1。

移码=真值+偏置值

为什么0.1可以输出0.1,但是0.1+0.2后却得到0.30000000000000004?

首先明确一点,我上面也提到了就是数据在计算机中是二进制形式存储的(遵循IEEE754二进制浮点数算术标准),而小数转二进制可能无限循环,IEEE754的尾数(给出了有效数字的位数)位数有限,需要舍入就产生了精度问题。

(1)0.1/0.2/0.3等变量在内存中的存储的二进制形式是固定的, 使得特定数值如0.1等能够被识别并正常打印。

比如:

1)0.1转二进制是无限循环,由于尾数位数限制计算机中存储的近似值是:

Decimal("0b"+0.1.toString(2)).toString();//0.1000000000000000055511151231257827021181583404541015625
0.1.toString(2)//转二进制字符串 '0.0001100110011001100110011001100110011001100110011001101'
Decimal("0b"+0.1.toString(2)).toNumber();//0.1
Decimal("0.1000000000000000055511151231257827021181583404541015625").toNumber();//0.1

2)0.2在计算机中存储的近似值是:

Decimal("0b"+0.2.toString(2)).toString();//0.200000000000000011102230246251565404236316680908203125
0.2.toString(2)//转二进制字符串 '0.001100110011001100110011001100110011001100110011001101'
Decimal("0b"+0.2.toString(2)).toNumber();//0.2
Decimal("0.200000000000000011102230246251565404236316680908203125").toNumber();//0.2

3)0.3在计算机中存储的近似值:

Decimal("0b"+0.3.toString(2)).toString();//'0.299999999999999988897769753748434595763683319091796875'
0.3.toString(2)//转二进制字符串 '0.010011001100110011001100110011001100110011001100110011'
Decimal("0b"+0.3.toString(2)).toNumber();//0.3
Decimal("0.299999999999999988897769753748434595763683319091796875").toNumber();//0.3

(2)0.1+0.2 在计算机中是按照存储的近似值来计算的:经过加法运算之后,叠加的误差导致计算结果为:

0.3000000000000000444089209850062616169452667236328125
Decimal("0.3000000000000000444089209850062616169452667236328125").toNumber();//0.30000000000000004
console.log(0.1+0.2);// 0.30000000000000004

以下是一些常见的解决方法:

1.先转为整数并计算然后再转回小数:

function add(num1, num2){
    const getDecimalLen = (numStr) => numStr.includes('.') ? (numStr.split('.')[1]).length : 0,//获取小数长度
    num1Len = getDecimalLen(num1.toString()),
    num2Len = getDecimalLen(num2.toString()),
    factor = Math.pow(10, Math.max(num1Len, num2Len));//10的maxLen次方
    return (num1 * factor + num2 * factor) / factor;
}
add(0.1, 0.2); // 0.3

2.使用专门的库或工具(js/python/java):

在处理需要高精度计算的场景中,可以使用一些专门的库或工具。例如,JavaScript 中的 Decimal.js、Big.js 或 BigNumber.js 等库提供了高精度的数学计算功能,可以避免精度丢失的问题。

(1)javascript这里主要介绍Decimal.js的使用:

var Decimal = require('decimal.js');
new Decimal(0.1).add(new Decimal(0.2)).toNumber(); //0.3

Decimal.js底层判断如果是number,会通过toString()转为string再构造:return parseDecimal(x, v.toString());

比如:通过Math.PI构造Decimal

new Decimal(Math.PI).toString();//'3.141592653589793'  因为底层是Math.PI.toString(): '3.141592653589793'构造的

我们可以这样从数字的底层二进制值创建一个实例

new Decimal(`0b${Math.PI.toString(2)}`).toString() //3.141592653589793115997963468544185161590576171875

如果想得到100位的PI:Decimal.set({ precision: 100 });  Decimal.acos(-1).toString();

只要数字的toString值的有效数字不超过 15 位,就不会出现任何问题。

如果使用位数过多的数字,建议传递字符串而不是数字,以避免潜在的精度损失。比如:

new Decimal(88259496234518.57).toString();即new Decimal(88259496234518.57.toString())// '88259496234518.56'
new Decimal("88259496234518.57").toString();//'88259496234518.57'

比如计算根号下(a的平方+b的平方)

Decimal.sqrt(_a.pow(2).add(_l.pow(2))).toString()  或  (_a.pow(2).add(_l.pow(2))).sqrt().toString()

(2)python中可以使用decimal模块处理高精度的数学计算。
from decimal import *

float(Decimal('0.1')+ Decimal('0.2')); # 0.3

(3)java中可使用jdk8的BigDecimal(不支持三角函数) 和 big-math拓展的BigDecimalMath(支持三角函数)处理高精度的数学计算。

 比如: 求2的平方根(保留6位精度):

BigDecimalMath.root(new BigDecimal(2), new BigDecimal(2) , new MathContext(6));

BigDecimal比较大小: a.compareTo(b) > 0 即a>b。   转double类型: a.doubleValue()

对于divide计算需要指定保留精度,否则除出来无限循环会报错:Non-terminating decimal expansion; no exact representable decimal result  

BigDecimal.divide(new BigDecimal(8), new MathContext(10));// 第二个参数传入MathContext指定精度
BigDecimal.divide(new BigDecimal(8), 4, BigDecimal.ROUND_HALF_UP);//第二个参数保留几位小数,第三个参数ROUND_HALF_UP为四舍五入规则(≥0.5进位)ROUND_HALF_DOWN为>0.5才进位。

(4)注意:可以传递给Decimal整型或者字符串参数,但不能是浮点数据(IEEE754二进制浮点数算术标准),因为浮点数据本身就不准确(小数转二进制可能无限循环)

1)Js中Decimal.js库由于底层会判断如果是number,自动通过toString()转为字符串然后再构造Decimal实例。故可直接传递number。只要数字的toString值的有效数字不超过 15 位,就不会出现任何问题。

如果使用位数过多的数字,建议还是传递字符串而不是数字,以避免潜在的精度损失。

new Decimal(88259496234518.57).toString();即new Decimal(88259496234518.57.toString())// '88259496234518.56'
new Decimal("88259496234518.57").toString();//'88259496234518.57'

2)java中BigDecimal需要传递整形或字符串,不能是浮点数

System.out.println(new BigDecimal(0.1+"").add(new BigDecimal(0.2+"")).doubleValue()); //0.3
System.out.println(new BigDecimal(0.1).add(new BigDecimal(0.2)).doubleValue());//0.30000000000000004
//使用位数过多的数字,建议直接传递字符串,而不是将数字转为字符串,以避免潜在的精度损失。
        System.out.println(new BigDecimal(88259496234518.57).toString());//88259496234518.5625
        System.out.println(new BigDecimal(88259496234518.57+"").toString());//88259496234518.56
        System.out.println(new BigDecimal("88259496234518.57").toString());//88259496234518.57

3)python中decimal需要传递整形或字符串,不能是浮点数:

    from decimal import *
    float(Decimal(0.1)+Decimal(0.2)) # 0.30000000000000004
    float(Decimal(str(0.1))+Decimal(str(0.2))) # 0.3
使用位数过多的数字,建议直接传递字符串,而不是将数字转为字符串,以避免潜在的精度损失。
str(Decimal(88259496234518.57)) #'88259496234518.5625' 
str(Decimal(str(88259496234518.57))) #'88259496234518.56'
str(Decimal('88259496234518.57')) #'88259496234518.57'

3.toFixed限制小数位数(返回字符串):

这种虽然简便,但是由于浮点数IEEE754规则的限制,不能够准确的达到四舍五入的效果。

1.35.toFixed(1); // '1.4' 正确

1.335.toFixed(2) // '1.33' 错误

解决toFixed问题:

转为整数,取舍后再转回小数, 但是Math.round对于负数的四舍五入处理时还存在一定问题。

Number.prototype.toFixed = function(size) {
    const padDecimal = (num, decimalPlaces) => {
        let numStr = num.toString();
        if (numStr.includes('.')) { //有小数,补0
            let decimalPart = numStr.split('.')[1];
            if (decimalPart.length < decimalPlaces) {
                numStr += '0'.repeat(decimalPlaces - decimalPart.length);
            }
        } else { // 如果没有小数部分,添加小数点和0
            numStr += '.';
            numStr += '0'.repeat(decimalPlaces);
        }
        return numStr;
    }
    return padDecimal((Math.round(this * Math.pow(10, size)) / Math.pow(10, size)), size);//还需要补充缺失的0
}

1.335.toFixed(2); //'1.34' 正确

(-1.335).toFixed(2);//'-1.33' 错误

-1.335.toFixed(2); //'-1.34' 正确 是因为'.'运算符优先级高于'-'

常见的舍入策略包括四舍五入Math.round、向上取整Math.ceil、向下取整Math.floor。

但是注意:Math.round对于负数的处理会存在一定问题。 floor和ceil不会出现问题。

Math.round(-5.4); // -5 正确

Math.round(-5.5); // -5 错误

Math.round(-5.6); // -6 正确

Math.round方法准确说是“四舍六入”,对0.5要进行判断对待。

Math.round的原理是对传入的参数+0.5之后,再floor向下取整得到的数就是返回的结果。

public static long round(double a) {
if (a != 0x1.fffffffffffffp-2) // greatest double value less than 0.5
    return (long)floor(a + 0.5d);
else
    return 0;
}

优化之后:

//先记录数值正负,保证round操作的是正数,在最后返回时,再进行修改正负

Number.prototype.toFixed = function(size) {
    const padDecimal = (num, decimalPlaces) => {
        let numStr = num.toString();
        if (numStr.includes('.')) { //有小数,补0
            let decimalPart = numStr.split('.')[1];
            if (decimalPart.length < decimalPlaces) {
                numStr += '0'.repeat(decimalPlaces - decimalPart.length);
            }
        } else { // 如果没有小数部分,添加小数点和0
            numStr += '.';
            numStr += '0'.repeat(decimalPlaces);
        }
        return numStr;
    }
    let abs = 1;
    if (this < 0) abs = -1;
    const _this = Math.abs(this);
    let factor = Math.pow(10, size);
    let result = Math.round(_this * factor) / factor;
    //补充缺失的0
    return padDecimal(result * abs, size);
}

1.335.toFixed(2); //'1.34' 正确

(-1.335).toFixed(2);//'-1.34' 正确

高等数学中一些基本概念

在数学和计算机中三角函数的所有计算都采用弧度制(π/2),而不是角度制。

其实,如果只是为了度量一个角度,用“角度”是满方便的.但是,在数学和工程技术中,大量用到三角函数和相关公式。用角度制的话相关公式比较复杂,不便计算。

使用弧度制的话, 公式是相对最简单的如下图所示:

radian 弧度   degress角度

三角函数(sin/cos/tan) 反三角函数等在计算机中一般传入的是弧度,而非角度。需转换(弧度=Math.PI/180 * 角度)。

exp,高等数学里以自然常数e为底的指数函数。exp(2)就是e的平方。

log:表示对数,与指数相反

root:表示开根计算。 sqrt 平方根  

在实数范围内负数的偶数次根(如平方根、四次根等)是没有定义的。这是因为没有实数乘以自身会得到负数。只有正数和0有平方根,正数的平方根互为相反数,0的平方根是0

只有在复数系内,负数的偶数次根才能被表示。负数的平方根为一对共轭纯虚数。例如:-1的平方根为±i,-9的平方根为±3i,其中i为虚数单位。

pow:表示幂,计算n次幂

add、subtract、multiple、divide 加减乘除

remainder %取余   negate 取反(-this)   abs 绝对值

‌Alpha(Αα)读音为“阿尔法”。‌Beta(Ββ)‌读音为“贝塔”。‌Gamma(Γγ)‌读音为“伽马”。‌

Delta(Δδ)‌读音为“德尔塔”。‌Epsilon(Εε)‌读音为“伊普西龙”。‌Zeta(Ζζ)‌读音为“截塔”。

Eta(Ηη)‌ 读音为“艾塔”。‌Theta(Θθ)‌ 读音为“西塔”。‌Iota(Ιι)‌ 读音为“约塔”。‌

Kappa(Κκ)‌ 读音为“卡帕”。‌Lambda(Λλ)‌ 读音为“兰布达”。‌Mu(Μμ)‌ 读音为“缪”。

‌Nu(Νν)‌ 读音为“纽”。‌Xi(Ξξ)‌ 读音为“克西”。‌Omicron(Οο)‌ 读音为“奥密克戎”。‌

Pi(Ππ)‌ 读音为“派”。‌Rho(Ρρ)‌ 读音为“肉”。‌Sigma(Σσ, ς)‌ 读音为“西格马”。

‌Tau(Ττ)‌ 读音为“套”。‌Upsilon(Υυ)‌ 读音为“宇普西龙”。Phi(Φφ)读音为“佛爱”。

Chi(Χχ)读音为“西”。Psi(Ψψ)读音为“普西”。Omega(Ωω)读音为“欧米伽”。

笛卡尔积:两个集合X和Y的笛卡尔积(Cartesian product),又称直积,表示为X × Y,第一个对象是X集合的成员而第二个对象是Y集合的成员所有可能有序对。

通俗的解释就是:用来表示两个团体中融合时候,两个团体中的每一个个体之间会产生什么样的可能性

Mysql中多表查询就是利用笛卡尔积生成一个虚拟表,然后再去除其中无关的记录。

微分、求导、积分相关概念:

  • 15
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

李庆政370

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值