「学习笔记」JS大数相加与大数相乘

2,135 阅读4分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第5天,点击查看活动详情

概述

我们的JS使用IEEE 754标准定义的64位浮点数格式表达数值,

也就是说在JS中想要表达数值范围在-2^532^53之间的所有整数,精度是没有问题的, 一旦数值超出这个范围,在数值末尾就会损失一定的精度

所以, 当我们计算两个数值相加, 而这两个数值之和超过JS能表示的最大安全数的情况下,就会存在精度丢失的问题

这也是为什么ES6 中引入BigInt的原因

大数相加:

示例

控制台中打印 console.log(12 + 12) , 此时可以得到 24, 是完全没有问题的。

JS能表达的最大安全整数Number.MAX_SAFE_INTEGER -> 9007199254740991

我们在控制到打印console.log(Number.MAX_SAFE_INTEGER + 12), 会得到这样的结果:

可以看出最后一位 1 + 2 是不可能等于4的, 说明此时就出现了精度丢失的问题

解决方案:

我们可以通过遍历数字的每一项, 让每一项去相加, 最后把每一项的到的结果结合起来, 就能达到我们想要的结果

但是数字是不能遍历的,所以最快捷的方法就是把他们转换成字符串, 然后去实现第一步 -> 遍历

确定基本思路之后,开始写代码

function bigSum(a, b){
  //接收到的是数字,先将他们转换成字符串
  a = '' + a
  b = '' + b
  //假如有一个为0, 直接返回不为0的一个
  if(a === '0' || b === '0') return a === '0' ? b : a
  //获得a和b 较长的一个,以为较短的一个前面补0,直到a和b长度相等
  const len = Math.max(a.length, b.length)
  a = a.padStart(len, '0');   // 000123
  b = b.padStart(len, '0');   // 123456
  //下面会用到的变量
  let t = 0,    //每次相加的结果
      f = 0,   //是否进位
      sum = ''   //最后的和
  //从后向前遍历
  for(let i = len - 1; i >= 0; i--){
     //此次运算的结果 + 上次是否有进位
     t = parseInt(a[i]) + parseInt(b[i]) + f
     //如果此次结果大于10, 说明下次可以进一位 
     //f只可能是1 或 0
     f = Math.floor(t / 10)
     //这一次加的永远是这次的个位, 如果此次结果>10, 会在下次进一位
     sum = t% 10 + sum
  }
  //如果遍历结束,发现f == 1 表示仍然可以进位, 就在sum最前面+1
  if(f){
      sum = f + sum
  }
  return sum
}

padStart用来填充字符串

此时来测试一下我们的代码

let a = Number.MAX_SAFE_INTEGER,     //9007199254740991
    b = 12
console.log(bigSum(a, b))

输出结果:

大数相乘

既然相加会有精度丢失的问题, 那么想乘同样也会存在这样的问题。

使用同样的思路, 转换成字符串解决一下。

其实这个是leetcode上的一道经典题目:

43 字符串相乘

解题思路

假设我们要计算 123 * 45, 我们的历程是这样的:

\

  • 先用个位的5 去和 3 2 1 相乘
  • 再用十位的4 去的 3 2 1 相乘
  • 最后错位相加, 得到计算结果

再细化一下就变成这样:

同样是错位相加

新建一个数组res去接收, 每次相加的值, 是否可以得到这样的规律

num1[i] num2[j]的乘积对应的就是 res[i + j] res[i + j + 1] 这两个位置

我们将res的每一项上的内容相加, 是不是就可以得到想要的结果了呢?

转换成代码,这样写:

function multiply(num1, num2){
  //首先转换成字符串
  num1 = '' + num1
  num2 = '' + num2
  if(num1 === '0' || num2 === '0') return '0'
  if(num1 === '1' || num2 === '1') return num1 === '1' ? num2 : num1
  let n1 = num1.length,
      n2 = num2.length
  //存储结果的数组 最长为n1 + n2
  let res = new Array(n1 + n2).fill(0)
  for(let i =  n1 - 1; i >= 0; i--){
    for(let j = n2 - 1; j >= 0; j--){
      let mul = (+num1[i]) * (+num2[j]),   //此时遍历到的两数之积
          p1 = i + j,
          p2 = i + j + 1,
          sum = mul + res[p2]   //第一次res[p2]一定是0, 加的是上一次如果有超过10的部分, 上一次的十位加在这一次的个位上
      //后面的位置
      res[p2] = sum % 10
      //sum如果有超过10的部分就加到上一位中, 下次进来, p1就会到p2的位置
      res[p1] += parseInt(sum / 10)
    }
  }
  //找到res中前面所有剩余的0
  let i = 0
  while(i < res.length && res[i] == 0){
    i++
  }
  //转换结果
  let str = ''
  while(i < res.length){
    str += res[i]
    i++
  }
  return str
}

参考资料:

mp.weixin.qq.com/s?__biz=MzA…