前言
在平时 coding 的时候,如果用到一些数学计算,那么加法和减法必然是我们代码中的常客,但是当一些数字超过 JS 可精确表示的范围时,我们又该如何计算呢?今天给大家带来的就是一个粗浅的算法思路及其实现。如果对我的内容有问题或者疑问,欢迎评论区多多交流。
理清思路
前置条件
- 首先要理解,因为 JS 无法准确表示超出
IEEE 754标准规范的整数。当数字超过-2^53到2^53的范围时,无法精确计算 ,因此大数加法和大数减法是利用string类型去实现的。 - 因为
string类型无法直接比较大小,需要有个方法比较大小,我这里抽象除了一个compare方法。 compare方法的流程如下,最终返回三种值,-1表示小于,1表示大于,0表示相等。- 首先比符号,负数肯定比正数小。
- 其次比位数,当符号相同那么从最高位数比,最高位大的负数更小,最高位大的正数更大。
- 每位数字从最高位比到最低位,如果中间没有比出大小说明两数相等。
- 加法有进位,减法有借位,这里用 carry 表示,在计算完最后需要做相应的处理,千万不能忘记
- 减法根据借位情况,可能会出现前缀
0,这时前缀0需要去除,但要注意不要把单独的结果0去除 。 - 当涉及正负符号的时候,可以先把正负符号的结果通过比较的方式计算出来,然后最后加上符号即可
流程图
为了更加直观的理解这个流程我画了两个简单的流程图
大数加
大数减(带正负号)
开始实现
1. compare 方法
compare 方法主要处理了四种比较的情况,负数和正数,正数和负数,负数和负数,正数和正数,前两种情况分别返回 -1 和 1 即可,后两种情况需要比对数位,前面的思路阐述中已经解释过,这里为了篇幅就把详细代码放在最后总结的代码中,这里不再赘述。
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}`;
细节和注意
-
底层的
minuse和add实现其实都相对简单,注意进位和借位即可 -
上层的
bigAdd和bigMinus需要比较大小,同符号和异符号可能会产生不同的效果 -
注意加法位数不同导致的问题,当其中任意一者还有内容的时候就必须继续加下去,且这个过程不能直接借助
carry处理 -
减法需要处理前缀
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;
}