🎉大数相加:从“算盘”到BigInt的奇幻之旅🎉

154 阅读8分钟

🧠 为什么我们需要大数相加?

在JavaScript的世界里,数字(Number)就像一个两面派——它既是整数,也是浮点数。但这位“两面派”有个致命弱点:当数值超过2^53-1(也就是9007199254740991)时,它就开始“装傻充愣”,把你的数字变成“乱码”

image.png

🧨 经典案例: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不行,那我们就用字符串来“扮演”大数!

🛠️ 核心思路

  1. 倒着遍历两个字符串的每一位数字(像小时候竖式加法)。
  2. 处理进位(比如5+6=11,进位1,保留1)。
  3. 补零(如果两个数字长度不同,短的前面补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  

为什么从右往左加?

  • 进位逻辑:个位相加产生的进位必须传递到十位,十位再传递到百位……
  • 顺序依赖:右边的计算结果会影响左边的值,所以必须从右往左“步步为营”。

🧩 代码中的“倒序遍历”

在字符串模拟中,我们通过倒序遍历(从末尾到开头)来还原这一过程:

  1. 定位最低位:字符串的最后一个字符是数字的“个位”。
  2. 逐位相加:从右往左处理每一位,确保进位能正确传递。
  3. 结果拼接:每次相加的结果放在结果的最前面(因为是从右往左处理的)。

🤯 如果“正着遍历”会怎样?

假设我们从左往右遍历:

// 错误示例(逻辑错误)  
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类型,专门用来处理超大整数。它的特点可以用一句话概括:“我管你是多大的数,我都能装得下!”

✅ 声明方式

  1. 后缀n
    let a = 123456789012345678901234567890123456789n;
    
  2. 函数转换(注意:必须用字符串!):
    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:虽然“力不从心”,但日常用它处理小数字还是绰绰有余的。

🚀 拓展学习


💡 最后送大家一句话

“大数相加不可怕,选对工具笑哈哈!BigInt一出,Number都得喊爸爸!” 😄