前言🏇🏇
在参加蓝桥杯的时候遇到了一个题目,这个题目考察了js中的精度损失问题,但是由于自己学艺不精,此前只是有所耳闻,但是从来没有去了解过如何解决,导致被这个点所困住而没有解出题目。
什么是精度损失?🍉🍉
比如说两个小数在相减或者相加造成结果和实际值有偏差,得到了一个和结果很接近但是不是想要得到的值的情况。
console.log(14.55-12.11)//结果是2.4400000000000013
比如正常情况下比如14.55-12.11结果是2.44,但是由于JavaScript是一门弱类型的语言,运算精度丢失的一个主要原因是浮点数的不精确性会造成精度的损失,使结果变成2.4400000000000013。
那么造成精度损失的原因都有那些呢?
- 有限的二进制表示位数:JavaScript 中的浮点数使用 64 位表示,其中一部分用于表示指数部分和符号位,而另一部分用于表示小数部分。由于有限的位数,无法精确表示所有的十进制数,特别是那些不能被二进制精确表示的分数,例如 0.1 或 0.2。
- 舍入误差:在进行浮点数运算时,由于位数的限制,可能会出现舍入误差。这是因为某些十进制数在二进制表示中可能是无限循环的,而在转换为有限的二进制表示时,会进行舍入操作,从而引入了误差。
如何解决精度损失呢?💬💬
由于JavaScript是一门弱类型的语言,那么不想C,Java
可以有专门的方式解决,我们最好的解决方式就是避免使用小数进行运算
- 如果我们要进行与金额有关的运算的时候,我们就可以先将位数扩大,讲小数转换为整数进行相关的运算,最后再除以相应的扩大的倍数即可。这样就可以有效的避免直接使用小数进行运算。
- 限制小数的位数,我们在js中可以使用toFixed方法来限制小数的位数,比如我们可以使用toFixed(2)将结果保留两位小数。
但是在JavaScript这些方法还是无法彻底的避免精度的损失问题只能尽量的避免,如果涉及到了对精度要求很高的计算,那么还是需要使用第三方的包bignumber.js 或decimal.js 来解决。
分享一下蓝桥杯题目⭐️⭐️
这个题目主要是讲的什么呢?其实就是我们最常用的红包功能:
输入红包的总金额和要发红包的个数,点击确定之后要求生成一个数组,数组中的每个元素代表每个红包的金额,要求每个红包的金额最低不能低于0.01元。当然每个红包最多保留两位小数,最终这些每个红包的金额加起来要等与红包的总金额
废话不多说,直接上代码
这部分代码是实现题目核心功能的代码,我将代码拆分成几部分来讲
function generateRedPacket(totalMoney, totalAmount) { // 将金额转换为以分为单位,避免小数直接运算 let MoneyTopoints = Math.round(totalMoney * 100); // 初始化红包数组 let redpacket = []; .....//核心代码部分 return redpacket; }
- 首先是第一部分代码,这部分代码就是进行了一个初始化的功能,首先将传递的
totalMoney
转换为分为单位,这样如果传递的数据有小数存在就会被转换为整数,使用了Math.round
方法,这个方法相当于是四舍五入的功能
会将小数转换为最近的整数,比如可以将10.5转换为11,将11.4转换为11,在函数的最后将的到的红包数组return回去
// 当前能分配的红包总金额 let maxRedpacket = MoneyTopoints; for (let i = 0; i < totalAmount; i++) { ....//特殊条件的判断的部分 // 生成随机的红包金额,并且将值转换为数字类型 let randomRedpacket = Math.floor((maxRedpacket * Math.random()) / (totalAmount - i)); // 如果小于0.01元,那么重新生成,这里是转换成分为单位了,所以小于1 while (randomRedpacket < 1) { randomRedpacket = Math.floor( (maxRedpacket * Math.random()) / (totalAmount - i) ); } ... ...//待补充部分 }
- 这部分代码就是比较核心的代码了,定义一个当前能分配的红包的最大金额,每次分出一个红包这个金额就要减少,然后我们需要生成随机的金额数目,通过
maxRedpacket * Math.random()
这部分代码实现,但是为什么后面还要除以totalAmount - i
呢? - 之所以这么做是因为题目要求不能使每次分出去的红包金额少于0.01,由于随机数具有不确定性,当
maxRedpacket
的值很大时候,有可能此时很出去的红包很大,造成后面的红包太小不符合要求,这样下面的while代码就会陷入死循环中,无论如何分红包还是小于1分,这样就会使程序卡死无法继续执行,因此为了避免单次分出去的红包过大,就做了这个处理。 - 其次
Math.floor(x)
这个方法的功能就是向下取整,会返回一个小于x的最大整数。 - 然后就是一个简单的判断,如果此次分出去的红包不符合要求,就要重新的进行红包的分配,直到满足要求为止。
redpacket.push(Number((randomRedpacket / 100).toFixed(2))); MoneyTopoints = MoneyTopoints - randomRedpacket;
- 紧接着在生成红包的金额数之后,我们还要进一步的处理,首先我们要/100是金额重新的转换为元,然后使用
toFixed
方法保留两位小数,但是注意,在调用toFixed
之后会返回一个字符串,因此我们要使用Number()
将其重新的转换为数字型的数据,然后就是使用push
方法将数据存进实现定义的数组redpacket
中。 - 然后我们要对
1MoneyTopoints
进行处理,对当前的最大金额进行处理,需要减去当前已经生成的红包金额,注意这里我们就巧妙的避免了直接使用小数来进行加减,有效的避免了js中的精度损失问题,这便是我们开始说的方法之一,
按目前来说程序好像已经没有什么问题,但是其实还是有问题的。
因为当程序执行到最后一个红包的时候,还是会执行maxRedpacket * Math.random()
进行随机数的相乘,这样就会使最后的到的所有红包金额加起来是不等于红包的总金额的,这显然是不对的,正确的做法其实这个时候maxRedpacket
就是最后一个红包的金额。应该直接进行赋值。
if (i === totalAmount - 1) { let randomRedpacket = maxRedpacket; redpacket.push(Number((randomRedpacket / 100).toFixed(2))); break; }
- 所以我在开头加上了一个判断条件判断是否是最后一个红包的分配,如果是最后一个红包的分配,那么就直接进行赋值,然后将值追加到数组当中直接break结束循环。
预览效果🏋️♀️🏋️♀️
大家可以自己加一下会发现最终结果等于输入值即为30
总结
在蓝桥杯考试中发现了很多自己的不足之处,还有很多需要提高的部分,一些基础的知识掌握的不到位,所以说学习不能只停留在表面,还是要深入的学习,这样才是真正的学会和掌握。