🧠 为什么我们需要大数相加?
在JavaScript的世界里,数字(Number)就像一个两面派——它既是整数,也是浮点数。但这位“两面派”有个致命弱点:当数值超过2^53-1(也就是9007199254740991)时,它就开始“装傻充愣”,把你的数字变成“乱码”。
🧨 经典案例:0.1 + 0.2 ≠ 0.3
console.log(0.1 + 0.2); // 输出 0.30000000000000004 😱
为什么会出现这种“反常识”的结果?
JavaScript使用IEEE 754双精度浮点数标准存储数字,所有数字都用64位表示,分为:
- 1位符号位(正负号)
- 11位指数位(决定数字的范围)
- 52位尾数位(决定精度,实际是53位,因为有一个隐含的1)
问题出在“二进制”上!
0.1和0.2在二进制中是无限循环小数(就像1/3在十进制中是0.333...),它们无法被精确表示。例如:
0.1的二进制是0.00011001100110011001100110011001100110011001100110011...0.2的二进制是0.0011001100110011001100110011001100110011001100110011...
当这两个“无限循环小数”被截断后存储到52位尾数中时,会像“四舍五入”一样产生微小误差。相加时,这些误差叠加,最终结果变成 0.30000000000000004,仿佛数字被“偷走了一点点”。
🧮 传统方法:字符串模拟大数相加
既然Number不行,那我们就用字符串来“扮演”大数!
🛠️ 核心思路
- 倒着遍历两个字符串的每一位数字(像小时候竖式加法)。
- 处理进位(比如5+6=11,进位1,保留1)。
- 补零(如果两个数字长度不同,短的前面补0)。
🧪 示例代码(字符串模拟)
/**
* @param {string} num1
* @param {string} num2
* @return {string}
*/
function addLargeNumber(num1, num2) {
let result = '';// 存储最终结果
let carry = 0;// 存储进位值
let i = num1.length - 1;// 指向第一个数字的末尾
let j = num2.length - 1;// 指向第二个数字的末尾
// 当还有数字需要处理或者还有进位时,继续循环
while (i >= 0 || j >= 0 || carry > 0) {
// 获取当前位的数字,如果已经处理完则取0
const digit1 = i >= 0 ? parseInt(num1[i]): 0;
const digit2 = j >= 0 ? parseInt(num2[j]): 0;
// 计算当前位的和(包括进位)
const sum = digit1 + digit2 + carry;
// 将当前位的计算结果(个位数)添加到结果的前面
result = (sum % 10) + result;
// 计算新的进位值
carry = Math.floor(sum / 10);
// 移动指针到前一位
i--;
j--;
}
return result;
}
// 测试一下
console.log(addLargeNumber("123456789", "987654321")); // 输出 1111111110 😄
🧨 解读
- 倒着遍历:像用算盘一样,从右往左拨动数字珠子。
- 进位处理:进位就像“小偷”,偷走10后只留下个位数。
- 补零:短的数字前面补零,就像给矮个子穿增高鞋,站队整齐才好看。
🚨 为什么字符串模拟大数相加要“倒着遍历”?
🧮 模拟“竖式加法”的精髓
小时候学加法时,老师是不是让你从右边开始加?比如:
123
+ 456
------
579
为什么从右往左加?
- 进位逻辑:个位相加产生的进位必须传递到十位,十位再传递到百位……
- 顺序依赖:右边的计算结果会影响左边的值,所以必须从右往左“步步为营”。
🧩 代码中的“倒序遍历”
在字符串模拟中,我们通过倒序遍历(从末尾到开头)来还原这一过程:
- 定位最低位:字符串的最后一个字符是数字的“个位”。
- 逐位相加:从右往左处理每一位,确保进位能正确传递。
- 结果拼接:每次相加的结果放在结果的最前面(因为是从右往左处理的)。
🤯 如果“正着遍历”会怎样?
假设我们从左往右遍历:
// 错误示例(逻辑错误)
function wrongAdd(num1, num2) {
let result = '';
let carry = 0;
for (let i = 0; i < num1.length; i++) {
const digit1 = parseInt(num1[i]);
const digit2 = parseInt(num2[i]);
const sum = digit1 + digit2 + carry;
result += (sum % 10);
carry = Math.floor(sum / 10);
}
return result;
}
问题在哪?
- 进位丢失:最高位的进位无法传递到更高级位(比如999 + 999 = 1998)。
- 结果错乱:个位、十位、百位的顺序完全颠倒,最终结果变成“反向数字”。
✅ 正确做法
倒序遍历+结果反转:
// 正确示例
function addLargeNumber(num1, num2) {
let result = '';
let carry = 0;
let i = num1.length - 1;
let j = num2.length - 1;
while (i >= 0 || j >= 0 || carry > 0) {
const digit1 = i >= 0 ? parseInt(num1[i--]) : 0;
const digit2 = j >= 0 ? parseInt(num2[j--]) : 0;
const sum = digit1 + digit2 + carry;
result = (sum % 10) + result;
carry = Math.floor(sum / 10);
}
return result;
}
🚨 为什么字符串模拟大数相加要“补零”?
🧬 补零的“物理意义”
在竖式加法中,两个数字的位数必须对齐,否则就像“短个子站在高个子旁边”,计算会出错。例如:
123
+ 4567
--------
如果不对齐,直接相加,结果会变成“123 + 4567 = 4690”(错误)!
🧪 补零的“代码实现”
在字符串模拟中,我们通过补零让两个数字长度一致:
function addLargeNumber(num1, num2) {
// 补零逻辑(自动补零)
const maxLength = Math.max(num1.length, num2.length);
num1 = num1.padStart(maxLength, '0');
num2 = num2.padStart(maxLength, '0');
// 倒序遍历 + 进位处理
let result = '';
let carry = 0;
for (let i = maxLength - 1; i >= 0; i--) {
const digit1 = parseInt(num1[i]);
const digit2 = parseInt(num2[i]);
const sum = digit1 + digit2 + carry;
result = (sum % 10) + result;
carry = Math.floor(sum / 10);
}
return result;
}
🤯 如果不补零会怎样?
后果很严重!
// 错误示例(不补零)
function wrongAdd(num1, num2) {
let result = '';
let carry = 0;
let i = num1.length - 1;
let j = num2.length - 1;
while (i >= 0 || j >= 0) {
const digit1 = i >= 0 ? parseInt(num1[i]) : 0;
const digit2 = j >= 0 ? parseInt(num2[j]) : 0;
const sum = digit1 + digit2 + carry;
result = (sum % 10) + result;
carry = Math.floor(sum / 10);
i--;
j--;
}
return result;
}
问题在哪?
- 高位缺失:短数字的高位会被忽略(比如“123”和“4567”相加时,“123”会被当作“0123”处理)。
- 结果错误:最终结果可能少一位或多位(比如999 + 999 = 1998,但不补零可能会变成“998”)。
✅ 正确做法
自动补零 + 对齐位数:
// 正确示例
function addLargeNumber(num1, num2) {
const maxLength = Math.max(num1.length, num2.length);
num1 = num1.padStart(maxLength, '0');
num2 = num2.padStart(maxLength, '0');
let result = '';
let carry = 0;
for (let i = maxLength - 1; i >= 0; i--) {
const digit1 = parseInt(num1[i]);
const digit2 = parseInt(num2[i]);
const sum = digit1 + digit2 + carry;
result = (sum % 10) + result;
carry = Math.floor(sum / 10);
}
return result;
}
🦸♂️ ES6救星:BigInt登场!
JavaScript在ES6中推出了BigInt类型,专门用来处理超大整数。它的特点可以用一句话概括:“我管你是多大的数,我都能装得下!”
✅ 声明方式
- 后缀n:
let a = 123456789012345678901234567890123456789n; - 函数转换(注意:必须用字符串!):
const theNum = BigInt("123456789012345678901234567890123456789");
🚫 禁忌操作
- 不能混合Number和BigInt运算:
console.log(1n + 2); // 报错!💥 console.log(1n + 2n); // 正确输出 3n ✅
🧪 示例代码(BigInt)
const bigNum = 123456789012345678901234567890123456789n;
console.log(bigNum + 1n); // 输出正确的大数 😎
🧨 解读
- BigInt:像一个“超级英雄”,专治Number的“不服”。
- 后缀n:这个“n”就像一个魔法咒语,瞬间让数字变成“金刚不坏之身”。
- 混合运算:Number和BigInt混搭?No way!就像油条配豆浆,必须同类型才能“和谐共处”。
⚠️ 注意事项:大数相加的“雷区”
1. Number的精度陷阱
- Number.MAX_SAFE_INTEGER:这是Number的“安全边界”,超过它就别指望精度了。
- IEEE 754标准:JS的Number基于这个标准,但它的52位尾数只能表示有限的整数。
2. 字符串处理的“暗坑”
- 补零操作:如果忘记补零,短数字的高位会被默认视为0,结果可能“跑偏”。
- 进位处理:进位忘记加到结果里,就像忘记给算盘珠子“进一位”,结果全错。
3. BigInt的“铁律”
- 不能与Number混用:这是BigInt的“底线”,强行混用会导致程序“爆炸”。
- 类型转换:用
BigInt()函数时,必须传入字符串,否则会“翻车”。
🌟 总结:大数相加的“武林秘籍”
| 方法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 字符串模拟 | 不依赖第三方库 | 代码稍复杂 | 低版本浏览器兼容性需求 |
| BigInt | 简洁高效,支持任意大数 | 需要ES6支持 | 现代项目、高性能需求 |
🧨 收尾
- 字符串模拟:像用算盘一样“手动操作”,适合喜欢“复古风”的开发者。
- BigInt:像给数字穿上“金钟罩”,直接“开挂”处理大数。
- Number:虽然“力不从心”,但日常用它处理小数字还是绰绰有余的。
🚀 拓展学习
- 深入理解IEEE 754标准:IEEE 754 Wikipedia
- BigInt官方文档:MDN BigInt
- 大数运算库推荐:
💡 最后送大家一句话:
“大数相加不可怕,选对工具笑哈哈!BigInt一出,Number都得喊爸爸!” 😄