大数如何加减?- 算法小练

328 阅读5分钟

前言

在平时 coding 的时候,如果用到一些数学计算,那么加法和减法必然是我们代码中的常客,但是当一些数字超过 JS 可精确表示的范围时,我们又该如何计算呢?今天给大家带来的就是一个粗浅的算法思路及其实现。如果对我的内容有问题或者疑问,欢迎评论区多多交流。

理清思路

前置条件

  1. 首先要理解,因为 JS 无法准确表示超出 IEEE 754 标准规范的整数。当数字超过 -2^532^53 的范围时,无法精确计算 ,因此大数加法和大数减法是利用 string 类型去实现的。
  2. 因为 string 类型无法直接比较大小,需要有个方法比较大小,我这里抽象除了一个 compare 方法。
  3. compare 方法的流程如下,最终返回三种值,-1 表示小于,1 表示大于,0 表示相等。
    • 首先比符号,负数肯定比正数小。
    • 其次比位数,当符号相同那么从最高位数比,最高位大的负数更小,最高位大的正数更大。
    • 每位数字从最高位比到最低位,如果中间没有比出大小说明两数相等
  4. 加法有进位,减法有借位,这里用 carry 表示,在计算完最后需要做相应的处理,千万不能忘记
  5. 减法根据借位情况,可能会出现前缀 0,这时前缀 0 需要去除,但要注意不要把单独的结果 0 去除 。
  6. 当涉及正负符号的时候,可以先把正负符号的结果通过比较的方式计算出来,然后最后加上符号即可

流程图

为了更加直观的理解这个流程我画了两个简单的流程图

大数加 image.png

大数减(带正负号)

image.png image.png

开始实现

1. compare 方法

compare 方法主要处理了四种比较的情况,负数和正数,正数和负数,负数和负数,正数和正数,前两种情况分别返回 -11 即可,后两种情况需要比对数位,前面的思路阐述中已经解释过,这里为了篇幅就把详细代码放在最后总结的代码中,这里不再赘述。

function compare(a1, a2) {
  if (a1.startsWith("-") && !a2.startsWith("-")) {
    return -1;
  } else if (!a1.startsWith("-") && a2.startsWith("-")) {
    return 1;
  } else if (a1.startsWith("-") && a2.startsWith("-")) {
    return compareNegative(a1, a2);
  } else if (!a1.startsWith("-") && !a2.startsWith("-")) {
    return comparePositive(a1, a2);
  }
  return 0;
}

2. 大数加

首先获取四个变量

  let carry = 0;  // 进位
  let i = a1.length - 1; // a1 的当前位指针,从最后一位开始
  let j = a2.length - 1; // a2 的当前位指针,从最后一位开始
  let arr = []; // 结果数据,最后会进行处理和组合

然后通过循环,将数字加起来

while (a1[i] || a2[j] || carry) { // 注意这里 a1 和 a2 位数可能不同,因此当有任意一个值,都要继续
  let x = 0; // 默认 0
  if (a1[i]) {
    x = Number(a1[i]);
  }
  let y = 0; // 默认 0
  if (a2[j]) {
    y = Number(a2[j]);
  }
  let res = (x + y + carry) % 10; // x + y 同时加上进位,对 10 求余,得到当前的加法结果
  arr.unshift(res); // 放到数组最前一位
  carry = Math.floor((x + y + carry) / 10); // 计算是否进位
  i--;  // 往前移动指针,准备下一位计算
  j--;
}

最后进行进位处理,返回字符串

  if (carry) { // 如果最后还有进位,需要把进位加上
    arr.unshift(carry);
  }
  return arr.join("");

3. 大数减(带正负号)

带正负号首先要确定结果的符号,以下三种情况都是负数,因此 s需要处理

  let s = ""; // 符号
  if (
    // 小减大,负数
    (a1[0] === "-" && a2[0] === "-") ||
    // 小减大,正数
    (a1[0] !== "-" && a2[0] !== "-")
  ) {
    if (compare(a1, a2) < 0) {
      // -123 - -12
      s = "-";
    }
  } else if (a1[0] === "-" && a2[0] !== "-") {
    // 负数减正数
    // -123 - 789
    s = "-";
  }

接着开始处理数字,前两种情况,分别是正数减负数,和负数减负数,抽象为去掉符号之后直接加上,即可得到数值结果

  let res = "";
  if (
    // 正 - 负
    (a1[0] !== "-" && a2[0] === "-") ||
    // 负 - 正
    (a1[0] === "-" && a2[0] !== "-")
  ) {
    if (a1[0].startsWith("-")) {
      a1 = a1.slice(1);
    }
    if (a2[0].startsWith("-")) {
      a2 = a2.slice(1);
    }
    res = bigAdd(a1, a2);
  } else if(
    ...
  ) {
    ...
  }

后两种情况,正数减正数,负数减负数,去掉符号后通过比较让大数减小数,即可得到数值结果

  if (
    ...
  ) {
  	...
  } else if (
    // 正 - 正
    (a1[0] !== "-" && a2[0] !== "-") ||
    // 负 - 负
    (a1[0] === "-" && a2[0] === "-")
  ) {
    if (a1[0].startsWith("-")) {
      a1 = a1.slice(1);
    }
    if (a2[0].startsWith("-")) {
      a2 = a2.slice(1);
    }
    if (compare(a1, a2) > 0) {
      res = minus(a1, a2);
    } else {
      let tmp = a1
      a1 = a2;
      a2 = tmp;
      res = minus(a1, a2);
    }
  }

最后通过符号加数值的方式返回结果

return `${s}${res}`;

细节和注意

  1. 底层的 minuseadd 实现其实都相对简单,注意进位和借位即可

  2. 上层的 bigAddbigMinus 需要比较大小,同符号和异符号可能会产生不同的效果

  3. 注意加法位数不同导致的问题,当其中任意一者还有内容的时候就必须继续加下去,且这个过程不能直接借助 carry 处理

  4. 减法需要处理前缀 0,同时要保证不能处理掉结果 0

总结

通过字符串的操作就能简单实现对大数的加法和减法,下篇文章计划实现对于大数的乘法和除法,最后的计算结果会更加复杂。

最后附上完整代码

function bigAdd(a1, a2) {
  let carry = 0;
  let i = a1.length - 1;
  let j = a2.length - 1;
  let arr = [];
  while (a1[i] || a2[j]) {
    let x = 0;
    if (a1[i]) {
      x = Number(a1[i]);
    }
    let y = 0;
    if (a2[j]) {
      y = Number(a2[j]);
    }
    let res = (x + y + carry) % 10;
    arr.unshift(res);
    carry = Math.floor((x + y + carry) / 10);
    i--;
    j--;
  }
  if (carry) {
    arr.unshift(carry);
  }
  return arr.join("");
}

// 123 23
// 23 - 123 = - (123 - 23) =
//

function bigMin(a1, a2) {
  // 负数减负数(去掉符号,大减小)(如果 a2 > a1,正号;如果 a1 > a2,负号)
  // 正数减负数(去掉符号,加起来)(正号)
  // 正数减正数(去掉符号,大减小)(如果 a2 > a1,负号;如果 a1 > a2,正号)
  // 负数减正数(去掉符号,加起来)(负号)
  let s = "";

  if (
    // 小减大,负数
    (a1[0] === "-" && a2[0] === "-") ||
    // 小减大,负数
    (a1[0] !== "-" && a2[0] !== "-")
  ) {
    if (compare(a1, a2) < 0) {
      // -123 - -12
      s = "-";
    }
  } else if (a1[0] === "-" && a2[0] !== "-") {
    // -123 - 789
    s = "-";
  }

  let res = "";
  if (
    // 正 - 负
    (a1[0] !== "-" && a2[0] === "-") ||
    // 负 - 正
    (a1[0] === "-" && a2[0] !== "-")
  ) {
    if (a1[0].startsWith("-")) {
      a1 = a1.slice(1);
    }
    if (a2[0].startsWith("-")) {
      a2 = a2.slice(1);
    }
    res = bigAdd(a1, a2);
  } else if (
    // 正 - 正
    (a1[0] !== "-" && a2[0] !== "-") ||
    // 负 - 负
    (a1[0] === "-" && a2[0] === "-")
  ) {
    if (a1[0].startsWith("-")) {
      a1 = a1.slice(1);
    }
    if (a2[0].startsWith("-")) {
      a2 = a2.slice(1);
    }
    if (compare(a1, a2) > 0) {
      res = minus(a1, a2);
    } else {
      let tmp = a1;
      a1 = a2;
      a2 = tmp;
      res = minus(a1, a2);
    }
  }

  return `${s}${res}`;
}

function minus(a1, a2) {
  let carry = 0;
  let i = a1.length - 1;
  let j = a2.length - 1;
  let arr = [];
  while (a1[i]) {
    let x = Number(a1[i]);
    let y = 0;
    if (a2[j]) {
      y = Number(a2[j]);
    }
    let temp;
    if (x - y + carry < 0) {
      temp = x + 10;
    } else {
      temp = x;
    }
    let res = temp - y + carry;
    if (x - y + carry < 0) {
      carry = -1;
    } else {
      carry = 0;
    }
    arr.unshift(res);
    i--;
    j--;
  }
  while (arr[0] === 0 && arr.length > 1) {
    arr.shift();
  }
  return arr.join("");
}

/**
 *
 * @param {string} a1
 * @param {string} a2
 * @returns {number}
 */
// 正负数,比较大数大小
function compare(a1, a2) {
  if (a1.startsWith("-") && !a2.startsWith("-")) {
    return -1;
  } else if (!a1.startsWith("-") && a2.startsWith("-")) {
    return 1;
  } else if (a1.startsWith("-") && a2.startsWith("-")) {
    return compareNegative(a1, a2);
  } else if (!a1.startsWith("-") && !a2.startsWith("-")) {
    return comparePositive(a1, a2);
  }
  return 0;
}

function compareNegative(a1, a2) {
  // 谁位数多谁小
  if (a1.length > a2.length) {
    return -1;
  } else if (a1.length < a2.length) {
    return 1;
  } else {
    let i = 0;
    while (a1[i] && a2[i]) {
      /// 123 789
      if (a1[i] > a2[i]) {
        return -1;
      } else if (a1[i] < a2[i]) {
        return 1;
      } else {
        i++;
      }
    }
  }
  return 0;
}

function comparePositive(a1, a2) {
  if (a1.length > a2.length) {
    return 1;
  } else if (a1.length < a2.length) {
    return -1;
  } else {
    let i = 0;
    while (a1[i] && a2[i]) {
      if (a1[i] > a2[i]) {
        return 1;
      } else if (a1[i] < a2[i]) {
        return -1;
      } else {
        i++;
      }
    }
  }
  return 0;
}