🔢 前言
Hello~大家好,我是秋天的一阵风
今天要攻克的是 LeetCode 上的经典 大数计算 题 ------「字符串相加」(题号 415)。
这道题的核心场景是「大数相加」:输入的两个非负整数以字符串形式存储(长度最长可达 5100 位),根本无法直接转成 Number 或 BigInt 类型计算,本质是考察手动模拟大数竖式加法的能力。
它和前两篇的「盛最多水」「接雨水」不同,重点不是算法复杂度优化,而是处理「进位、长度对齐、末尾残留进位」这些大数计算的关键细节,非常适合夯实字符串操作和边界处理思维。
话不多说,咱们一步步拆解,让你彻底掌握大数相加的核心逻辑~
一、LeetCode 大数相加(字符串版)题目详情
1. 题目描述
给定两个非负整数 num1 和 num2,它们以字符串形式表示(即大数),返回它们的和也以字符串形式表示。说明:
- 你不能使用任何内置的 BigInteger 库或直接将输入转换为整数形式(核心限制,凸显大数场景);
num1和num2的长度都小于 5100(明确大数规模);num1和num2都只包含数字0-9;num1和num2都不包含前导零(除了数字 0 本身)。
题目链接 :415. 字符串相加 - 力扣(LeetCode)
2. 示例演示
- 输入:
num1 = "11", num2 = "123" - 输出:
"134" - 解释:11 + 123 = 134,模拟竖式相加:个位 1+3=4,十位 1+2=3,百位 0+1=1,拼接结果为 "134"(小型大数场景,理解基础逻辑)。
- 输入:
num1 = "456", num2 = "77" - 输出:
"533" - 解释:个位 6+7=13(留 3 进 1),十位 5+7+1=13(留 3 进 1),百位 4+0+1=5,结果为 "533"(含进位的典型场景)。
- 输入:
num1 = "999999999999999999", num2 = "1" - 输出:
"1000000000000000000" - 解释:超长大数相加,末尾进位贯穿所有位,最终需在最前方补 1(大数计算核心边界场景)。
- 输入:
num1 = "0", num2 = "0" - 输出:
"0" - 解释:两个零相加,结果仍为零,注意不能返回 "00" 这类前导零(特殊边界场景)。
3. 难度级别
🟢 简单 → 🔵 中等(实际考察):题目逻辑本身不复杂,但大数场景下的「进位传递」「长度对齐补零」「末尾残留进位」这三个点极易出错,核心是复刻竖式加法的完整流程,确保覆盖所有大数计算的边界情况。
二、解题思路大剖析
1. 基础解法:模拟大数竖式相加
基础解法的核心思路就是复刻大数竖式加法的手工流程:因为是大数,无法直接转数字计算,所以从两个字符串的「末尾(个位)」开始,逐位提取数字相加,同步记录当前位结果和进位,最后将结果反转(因计算顺序是从低位到高位)。
核心步骤:
-
指针初始化:
i指向num1末尾(个位),j指向num2末尾(个位),适配大数的低位到高位计算逻辑; -
进位初始化:
carry = 0(初始无进位,大数相加的进位可能贯穿多位); -
结果容器:用数组
res存储每一位结果(大数拼接频繁,数组比字符串高效); -
循环计算(覆盖大数所有位 + 残留进位):只要
i >= 0(num1 未处理完)、j >= 0(num2 未处理完)或carry > 0(仍有进位),就继续:- 提取当前位数字:num1 当前位为
i >= 0 ? num1[i] - '0' : 0(大数长度不一致时,短数高位补 0),num2 同理; - 计算当前位总和:
sum = 位1 + 位2 + carry(必须包含前一位进位,大数进位不可遗漏); - 提取当前位结果:
sum % 10(取个位,如 sum=13 则当前位为 3); - 更新进位:
carry = Math.floor(sum / 10)(取十位,如 sum=13 则进位为 1,可能传递到下一位); - 存入结果:将当前位结果推入
res数组; - 指针左移:
i--、j--,处理大数的更高位;
- 提取当前位数字:num1 当前位为
-
结果整理:
res中是「个位→高位」的顺序,反转后拼接成字符串(大数的高位在前、低位在后)。
分步拆解演示(以大数输入 num1="9999", num2="123" 为例):
-
初始状态:
i=3(num1[3]='9'),j=2(num2[2]='3'),carry=0,res=[]; -
第 1 轮(个位):
- 位 1=9,位 2=3 → sum=9+3+0=12;
- 当前位:12%10=2 → res=[2];
- 进位:12/10=1 → carry=1;
- 指针:i=2,j=1;
-
第 2 轮(十位):
- 位 1=9(num1 [2]='9'),位 2=2(num2 [1]='2') → sum=9+2+1=12;
- 当前位:12%10=2 → res=[2,2];
- 进位:12/10=1 → carry=1;
- 指针:i=1,j=0;
-
第 3 轮(百位):
- 位 1=9(num1 [1]='9'),位 2=1(num2 [0]='1') → sum=9+1+1=11;
- 当前位:11%10=1 → res=[2,2,1];
- 进位:11/10=1 → carry=1;
- 指针:i=0,j=-1;
-
第 4 轮(千位):
- 位 1=9(num1 [0]='9'),位 2=0(j<0 补 0) → sum=9+0+1=10;
- 当前位:10%10=0 → res=[2,2,1,0];
- 进位:10/10=1 → carry=1;
- 指针:i=-1,j=-1;
-
第 5 轮(残留进位):
- 位 1=0,位 2=0 → sum=0+0+1=1;
- 当前位:1%10=1 → res=[2,2,1,0,1];
- 进位:1/10=0 → carry=0;
- 指针:i=-1,j=-1;
-
循环终止:i<0、j<0 且 carry=0;
-
反转 res:[2,2,1,0,1] → [1,0,1,2,2] → 拼接成字符串 "10122"(9999+123=10122,符合大数计算预期)。
JavaScript 代码实现(基础解法):
js
/**
* @param {string} num1
* @param {string} num2
* @return {string}
*/
var addStrings = function(num1, num2) {
let i = num1.length - 1; // 指向num1末尾(大数个位)
let j = num2.length - 1; // 指向num2末尾(大数个位)
let carry = 0; // 进位,大数相加可能跨多位传递
const res = []; // 存储每一位结果,避免大数字符串频繁拼接
// 循环条件:覆盖大数所有位 + 残留进位
while (i >= 0 || j >= 0 || carry > 0) {
// 提取当前位数字(大数长度不一致时补0),字符转数字(减'0')
const digit1 = i >= 0 ? num1[i] - '0' : 0;
const digit2 = j >= 0 ? num2[j] - '0' : 0;
// 计算当前位总和(含前一位进位)
const sum = digit1 + digit2 + carry;
// 当前位结果:sum的个位数
const currentDigit = sum % 10;
// 更新进位:sum的十位数(向下取整,可能为0或1)
carry = Math.floor(sum / 10);
// 推入结果数组(大数低位→高位顺序)
res.push(currentDigit);
// 指针左移,处理大数更高位
i--;
j--;
}
// 反转数组→拼接字符串(大数高位→低位顺序)
return res.reverse().join('');
};
// 测试用例验证(覆盖大数、进位、边界场景)
console.log(addStrings("11", "123")); // 输出"134",符合预期
console.log(addStrings("9999", "123")); // 输出"10122",符合预期
console.log(addStrings("999999999999999999", "1")); // 输出"1000000000000000000",符合预期
console.log(addStrings("0", "0")); // 输出"0",符合预期
基础解法的优缺点:
- 优点:完全贴合大数竖式加法逻辑,步骤清晰,覆盖所有大数场景的边界(超长长度、跨位进位、残留进位),面试中写出来稳定性高,不易出错;
- 缺点:代码有少量冗余变量(如
digit1digit2),但不影响可读性,大数计算场景下时间和空间已接近最优,无明显可优化点。
2. 优化解法:代码简洁化
优化解法的核心逻辑和基础解法完全一致(仍是模拟大数竖式),仅在代码写法上精简,减少冗余变量,让代码更紧凑(面试中能体现对大数计算逻辑的熟练掌握)。
优化点:
- 合并变量声明:将
ijcarry合并声明,减少代码行数; - 嵌入位计算:将
digit1digit2的提取直接嵌入sum计算中,避免冗余变量; - 简化循环条件:
carry为 0 时会自动终止,无需写carry > 0(因 0 为 falsy 值)。
JavaScript 代码实现(优化解法):
js
/**
* @param {string} num1
* @param {string} num2
* @return {string}
*/
var addStrings = function(num1, num2) {
let i = num1.length - 1, j = num2.length - 1, carry = 0;
const res = [];
while (i >= 0 || j >= 0 || carry) {
// 直接计算当前位总和(嵌入大数位提取+补0逻辑)
const sum = (i >= 0 ? num1[i] - '0' : 0) + (j >= 0 ? num2[j] - '0' : 0) + carry;
res.push(sum % 10); // 当前位结果
carry = Math.floor(sum / 10); // 更新进位
i--;
j--;
}
return res.reverse().join('');
};
// 测试用例验证
console.log(addStrings("456", "77")); // 输出"533",符合预期
console.log(addStrings("999999999999999999", "1")); // 输出"1000000000000000000",符合预期
优化解法的特点:
- 逻辑不变:完全遵循大数竖式加法规则,覆盖所有边界场景;
- 代码精炼:行数减少,无冗余变量,面试时书写速度更快;
- 可读性强:变量名自解释,面试官能快速理解大数计算逻辑;
- 复杂度不变:时间和空间复杂度与基础解法一致,属于「写法优化」而非「算法优化」。
三、总结
1. 核心逻辑
大数相加(字符串版)的本质是「模拟手工竖式加法」,核心要点有三个,缺一不可:
- 「从后往前算」:大数的低位在字符串末尾,需从末尾开始逐位处理;
- 「补零对齐」:大数长度不一致时,短数的高位补 0,避免索引越界,确保每一位都能对应相加;
- 「进位不遗漏」:每一步相加必须带上前一位的进位,且循环结束前需检查是否有残留进位(如 999+1 的最后进位 1)。
2. 最后
今天的「大数相加(字符串版)」就讲解到这里啦!相信大家已经吃透了「模拟竖式 + 进位传递」的核心逻辑,不管是基础解法还是优化解法,都能轻松应对面试中的大数场景。如果在测试超长大数、全 9 数字相加等特殊情况时遇到问题,或者有更巧妙的实现思路,欢迎在评论区留言讨论~
下一篇,咱们会继续攻克 LeetCode 高频题(「三数之和」),关注我,刷题路上不迷路!咱们下期再见~ 👋