一文搞懂 0.1+0.2 浮点数计算和解决方案
1.为什么 0.1+ 0.2 != 0.3?
计算机是通过二进制的方式存储数据的,所以计算机计算 0.1 + 0.2 的时候,实际上是计算的两个数的二进制的和。
在计算机中,浮点数的表示是有限的,而且它们的二进制表示可能无法精确地表示一些十进制分数,这可能导致一些浮点数计算不符合我们预期的精确性。并且0.1
和 0.2
在浮点数表示中是近似值,它们的二进制表示并不能精确地表示这些小数。因此,当进行浮点数计算时,可能会出现舍入误差,从而导致类似 0.1 + 0.2 !== 0.3
的情况发生。——> 一句话:浮点数的计算会转化为二进制,但是它们的二进制表示是四舍五入的,所以不精确
所以说 0.1+0.2 不等于 0.3,本质上是因为 0.1 和 0.2 都无法精确表示,那么它俩是怎么表示的呢?
0.1 和 0.2 都无法精确表示的原因与二进制数的特性有关。在十进制中,0.1 和 0.2 都是有限的小数,分别表示为 1/10 和 2/10。然而,在二进制中,它们被转换为无限循环小数。
为了更好地理解这一点,我们可以看一下它们的二进制表示:
0.1 的二进制表示约为:0.00011001100110011001100110011001100110011001100110011...
0.2 的二进制表示约为:0.0011001100110011001100110011001100110011001100110011...
在这两种情况下,我们可以看到它们的二进制表示是无限循环的,永远不会终止。然而,计算机的存储是有限的,不能无限精确地表示这些无限循环的二进制小数。
当进行 0.1 + 0.2 这样的计算时,实际上是对近似值进行运算,而非精确的 0.1 和 0.2!所以结果当然不准确!
这种舍入误差并不是 JavaScript 特有的问题,而是所有使用 IEEE 754 浮点数标准的编程语言和系统都会面临的挑战。为了避免精度损失,当需要进行精确的小数计算时,建议使用整数运算或采用特定的高精度数值库来处理。
2.IEEE 754 标准浮点数
IEEE 754 浮点数表示法介绍,小数不能精确保存的本质原因?
原因:主要是使用IEEE754 标准的浮点数的存储问题,浮点数小数位数最多52位!
在 IEEE754 标准中常见的浮点数数值表示有:单精准度(32 位)和双精准度(64 位),而 JS 采用的是双精度版本,也就是通过 64 位来表示一个数字;在二进制科学表示法中,双精度浮点数的小数部分最多只能保留 52
位,再加上.前面的(0 或 1),其实就是保留 53
位有效数字,剩余的需要舍去,遵从“0 舍 1 入
”的原则。
由于 IEEE 754 浮点数表示法使用有限的二进制位数来表示实数,因此在某些情况下会导致舍入误差和精度损失。这就是为什么在进行浮点数运算时,可能会出现意外的结果。对于关键的高精度计算,通常需要使用其他方法来确保计算的准确性。
二进制无法精确表示十进制小数:很多十进制小数在二进制中无法精确表示。
可以被精确表示的小数通常是二进制的分数,即分母为 2 的幂次的小数。例如,1/2、1/4、1/8 等等都可以在二进制中精确表示。这是因为这些分数可以被精确地表示为有限位的二进制小数。
然而,对于其他分数,尤其是无法精确表示为分母为 2 的幂次的分数,可能会出现精度丢失。例如,0.1(十进制)在二进制中无法被精确表示,因为它是一个无限循环小数:0.000110011001100...。由于计算机中的浮点数通常使用固定数量的位来表示,所以可能会截断或近似这种无限循环的小数,导致精度损失,实际上存储的是一个近似值,而非精确的 0.1。
浮点数精度:IEEE 754 标准使用一定的位数来表示浮点数,例如,64 位的双精度浮点数中,52 位用于尾数,11 位用于指数,1 位用于符号位。浮点数的表示范围是有限的,当某个数超出了这个范围时,它会被近似为最接近的可表示值,这就引入了舍入误差。
既然 IEEE 754 有一定的弊端(它不能精确表示所有的小数),那么使用 IEEE 754 的原因是什么呢?
IEEE 754 浮点数表示法通过指数和尾数的编码,使得浮点数能够以科学计数法形式表示,从而能够有效地表示极大或极小的实数值。
IEEE 754 浮点数表示法有两种格式:单精度(32 位)和双精度(64 位)。在双精度表示法中,位数分配如下:
- 1 位符号位,符号位用来表示浮点数的正负,即小数点前面的那一位
- 11 位指数位
- 52 位尾数位
通过这种结构,浮点数可以以科学计数法形式表示,即 尾数 * 2^指数
。指数允许浮点数覆盖很大范围,而尾数则允许控制精度。
例如,双精度浮点数可以表示像 1.2345 x 10^27 这样的大数,也可以表示像 1.2345 x 10^-27 这样的小数,同时还可以控制小数的精度。(这样实际上 27 只占用了指数的几位二进制位而已,但是却能表达 10^27 这么大的数!)
尽管 IEEE 754 浮点数表示法非常强大,但它也存在精度问题,特别是在处理小数时可能会引起舍入误差。因此,在计算机编程中,需要注意浮点数运算的精度问题,以避免产生不准确的结果。
不论整数和小数都是通过 IEEE 754 标准保存的!
IEEE 754 标准适用于计算机中的浮点数表示,无论是整数还是小数,都会被表示为浮点数的形式。这是因为在计算机中,整数和小数都可以被看作浮点数的特殊情况。
对于整数,它可以被看作指数为 0 的浮点数,而尾数部分包含整数的二进制表示。例如,整数 5 可以表示为 5.0 x 2^0,转化为二进制就是 0.01111111111 101…。
对于小数,它会被表示为指数不为 0 的浮点数,而尾数部分则包含小数的二进制分数表示。例如,0.125 可以表示为 1.0 x 2^-3。
3.如何让 0.1+0.2 和 0.3 相等?
如何实现让 0.1+0.2 和 0.3 相等?也就是说不知道结果是否为 0.3 时,想判断一下是否是 0.3?
1.使用 toFixed()
最简单的方法,四舍五入,只保留小数点后 1 位,注意 toFixed(n)返回值的类型是 string 类型,所以还需要用 parseFloat 转换为小数才可以。
优势 toFixed 会对原始数字进行四舍五入,并将结果格式化为指定的小数位数,确保返回的结果在计算时保持准确性!
parseFloat((0.1 + 0.2).toFixed(1)) === 0.3; // true
2.将其转换为整数后在进行运算
运算后再转为对应的小数。
(0.1 * 100 + 0.2 * 100) / 100 === 0.3; // true
3.使用 Math.abs()和 Number.EPSILON
可以使用一个很小的误差范围来比较两个浮点数是否接近相等,而不是直接比较它们是否完全相等。这被称为“浮点数比较”或“浮点数近似相等”。
一个直接的解决方法就是设置一个误差范围,通常称为“机器精度”。对 JavaScript 来说,这个值通常为 2-52,在 ES6 中,提供了 Number.EPSILON 属性,而它的值就是 2-52,只要判断 0.1+0.2-0.3 是否小于 Number.EPSILON,如果小于,就可以判断为 0.1 + 0.2 === 0.3。
相加值与目标值之间的误差的绝对值小于机器精度,即可判断为相等。
function numberepsilon(arg1, arg2) {
return Math.abs(arg1 - arg2) < Number.EPSILON; //伊普斯楞
}
console.log(numberepsilon(0.1 + 0.2, 0.3)); // true
4.高精度计算的解决方案
JS 中进行高精度的计算的解决方式:BigNumber 库
也就是说可以精确计算出 0.1+0.2 的结果,也就是 0.3!
JavaScript 中的 BigNumber.js:
BigNumber.js 是一个用于处理高精度数值的 JavaScript 库,它可以处理超出标准浮点数范围的数值,并提供高精度的运算功能。
const BigNumber = require("bignumber.js");
const x = new BigNumber("0.1");
const y = new BigNumber("0.2");
const result = x.plus(y);
console.log(result.toString()); // 输出 "0.3"
5.从[0,i]内生成 n 个不重复的随机数
1.最高级的解法:洗牌算法
使用洗牌算法:
如果你希望生成不重复的随机数,你可以使用洗牌算法(Fisher-Yates Shuffle)来随机打乱一个指定范围内的整数,然后取前面的若干个数作为结果。这样就可以确保生成的随机数不重复。
以下是一个生成不重复的随机数的示例:
function generateNonRepeatingRandomNumbers(count, maxNumber) {
const numbers = [];
// 生成 0 到 maxNumber 之间的所有整数
for (let i = 0; i <= maxNumber; i++) {
numbers.push(i);
}
// 使用洗牌算法打乱数组顺序 ——> 关键!
//洗牌算法:遍历每一位,把每一位与除此之外的随机一位进行交换)
for (let i = numbers.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[numbers[i], numbers[j]] = [numbers[j], numbers[i]];
}
// 取前面 count 个数作为结果
return numbers.slice(0, count);
}
const randomNumbersArray = generateNonRepeatingRandomNumbers(10, 100);
console.log(randomNumbersArray);
2.我自己的解法:for 循环并使用数组记录避免重复:简单
我自己写的从[0,100]内取不重复的 10 个随机数的方法:
这种写法很不好,时间复杂度是 O(n2),并且 toFixed(0)是四舍五入,可能会造成无法输出到 100,不如使用 101 多取一位 + Math.floor 向下取整的方式!
const test = () => {
let result = [];
for (let i = 0; i < 10; i++) {
let num = Number((Math.random().toFixed(2) * 100).toFixed(0));
// console.log(num);
while (result.includes(num)) {
//判断是否已经有过这个数字
num = Number((Math.random().toFixed(2) * 100).toFixed(0));
}
result.push(num);
}
return result;
};
console.log(test());
3.递归解法:难以理解,但是很重要
递归的思想总结:
1.要明确一个递归方法里面一般都是有两个返回值的,一个是最终我们要的结果(需要明确这个返回值会是什么东西),一个是临界条件也就是递归终止条件的返回值
2.我们要的结果基本上都是通过递归终止的返回值拼接而成的,也可以等于递归终止的返回值!
这里就是我们要的结果等于递归终止的返回值的情况!
3.还有一个重要部分就是递归函数的调用,这个函数的调用有的时候可以和最终结果放在一起,一起 return,这里就是这样!
const test = (result = []) => {
//这里初始条件要给一个空数组,这样也不用在函数外面维护空数组了!或者说就不用写成函数套函数了,不用闭包了!
if (result.length === 10) {
return result;
}
let num = Number((Math.random().toFixed(2) * 100).toFixed(0));
// console.log(num);
//while (result.includes(num)) { //判断是否已经有过这个数字,有的话就循环直到生成一个没有的 (因为我们是递归的,所以不需要,只需要一直递归就可以了)
//num = Number((Math.random().toFixed(2) * 100).toFixed(0));
//}
if (!result.includes(num)) {
//判断是否已经有过这个数字,有的话就接着递归
result.push(num);
}
return test(result);
};
console.log(test());
4.生成[0,i]内的一个随机整数的通用方法:Math.floor(Math.random() * (i + 1))
核心:生成[0,i]内的一个随机整数的方法:
Math.floor(Math.random() * (i + 1))
是一个常用的形式,它的解释:
Math.floor(Math.random() * (i + 1))是一个常见的用于生成介于 [0, i] 范围内随机整数的方法。
让我们来逐步解释这个表达式:
Math.random()
:Math.random()
是 JavaScript 中的一个内置函数,它返回一个 0(包括)到 1(不包括)之间的随机小数。也就是说,它返回一个范围为 [0, 1) 的随机数。Math.random() * (i + 1)
: 这一步是将随机小数值乘以(i + 1)
,其中i
是当前循环迭代的次数,也就是当前数组的索引。因为Math.random()
返回的是 [0, 1) 的随机小数,所以乘以(i + 1)
之后,得到的结果就是一个介于 [0, i+1) 的随机数。Math.floor(Math.random() * (i + 1))
:Math.floor()
是 JavaScript 中的一个内置函数,它将传入的数值向下取整,即去掉小数部分。这样,Math.floor(Math.random() * (i + 1))
就得到了一个介于 [0, i] 的随机整数。
5.toFixed(0) 和
Math.floor() 的对比?
toFixed(0)
适用于处理带有小数的数字,并将其四舍五入到最接近的整数,返回一个字符串表示该整数。Math.floor()
适用于将任意数值向下取整,返回一个整数。
因此,如果你需要处理带有小数的数字并四舍五入到最接近的整数,你可以使用 toFixed(0)
。如果你只是需要将一个数字向下取整并得到整数部分,你可以使用 Math.floor()
。
6.为什么toFixed()
方法会将数字转换为字符串类型?
toFixed()
方法返回字符串的原因是为了确保精度和保留小数位数的准确性。
JavaScript 中的数字类型(Number
)使用 IEEE 754 浮点数表示法来存储和处理数字,这种表示法对于大多数情况是有效的,但对于某些特定的数字和计算,可能会导致精度损失或舍入错误。
toFixed()
方法的设计目的是提供一种简单的方式来处理精确小数位数,并避免由于浮点数计算带来的精度问题。当你使用 toFixed()
来指定保留的小数位数时,它会对原始数字进行四舍五入,并将结果格式化为指定的小数位数,确保返回的结果在计算时保持准确性。
返回字符串而不是数字的好处在于,字符串的表示形式不受 JavaScript 中数字类型的精度限制。这样可以确保 toFixed()
方法返回的结果在任何计算或显示中都能够保持准确(否则 0.3 保存的时候还是不准确的,至少字符串我们可以确保这次输出准确),而不会出现精度丢失或舍入错误。
如果需要将 toFixed()
方法返回的字符串转换回数字类型,你可以使用 Number()
函数或 parseFloat()
函数来实现
6.难度升级:从[min,max]内生成不重复的 n 个随机数
1.公式:牢记
Math.floor(Math.random() * (max - min + 1)) + min;
解释:
Math.random() * (max - min + 1)
: 这部分计算一个介于 0 和max - min + 1
之间的随机浮点数。这个数在范围内的每个整数都有相同的概率。Math.floor(Math.random() * (max - min + 1))
: 这部分将上述计算得到的随机浮点数向下取整,得到一个介于 0 和max - min
之间的整数。Math.floor(Math.random() * (max - min + 1)) + min
: 最后,将上述计算得到的整数加上min
,得到一个介于min
和max
之间的随机整数。——> 相当于生成的区间范围整体向右平移 min 个单位!
理解:最大范围先减一下 min,然后整体平移 min 即可,然后就保证了最小最大范围都准确!