题目
给定两个字符串形式的非负整数
num1 和
num2,计算它们的和并同样以字符串形式返回。
你不能使用任何内建的用于处理大整数的库(比如 BigInteger),也不能直接将输入的字符串转换为整数形式。
示例:
输入: num1 = "456", num2 = "77"
输出: "533"
腾讯面试高频(TOP 9 常客),还有两个常见延伸:大数相加
含负数(符号处理 + 转减法),以及进阶的
大数相乘(LC 43)。这篇把主解法和两个延伸一次讲透。
一、核心洞察:不是算法题,是翻译题
这题的送分处和难点是同一句话:
"不能转整数"不是刁难,而是排除法 —— 它把「先转数字再算」的捷径全部排除,剩下的唯一解法就是你小学二年级学的列竖式。
面试官想看的就是这件事:
你能不能把一个"生活里已经会"的过程,一丝不漏地翻译成代码。竖式人人会列,但翻译成代码时几乎人人漏东西 —— 漏进位、漏补零、漏最后一位的进位。所以这题考的不是"想出算法",而是
写的完整度。
竖式的过程拆开看,每一列只有一次决策:
1 1 ← 进位行(手算时写在顶上的小 1)
4 5 6
+ 7 7
-------
5 3 3 ← 从个位往高位,一列一列算出来
个位:6 + 7 = 13,
写下 3,进位 1 带上一位;十位:5 + 7 + 1 = 13,写下 3 进 1;百位:4 + 0 + 1 = 5(num2 没有百位,
补 0),写下 5 进 0 —— 结束。
三个关键词:
从个位开始(末尾对齐)、写下个位带走进位、缺位补零。翻译成代码就是双指针从两个串的末尾出发,加上一个
carry 变量。
二、拆解:三个实现要点
2.1 对齐:下标判断补零,不动原串
两个串长度不等,怎么对齐数位?两种做法:
选后者。
i < 0 表示 num1 这一位已经用完,
这一列它贡献 0 —— 注意是"贡献 0",不是"跳过这列":列还在,只是这一侧的加数是 0。
2.2 每一列:加、写、进
sum = a + b + carry ← a、b 是这一列的两个数字(含补零),carry 是低位带上来的
out = sum % 10 ← 写下的本位
carry = sum >= 10 ? 1 : 0 ← 带上一位的进位(两个非负一位数相加,进位最多是 1)
为什么进位最多是 1?
9 + 9 + 1 = 19,就算两个加数都是 9、低位还进 1,和也不会到 20 —— 所以
carry 是 0/1 变量,不是"除以 10 取整"的一般化。这是非负十进制竖式的特例,写代码时可以直接利用。
2.3 循环条件:三连 or
这是
全题最容易漏的一处:
while i >= 0 or j >= 0 or carry > 0: # 三连 or,一个都不能少
前两个条件管"还有数位没加",
carry > 0 管"最后还有进位要长出一位"。漏掉
carry 的经典翻车现场:
"999" + "1":
9+1=10 写 0 进 1;9+0+1=10 写 0 进 1;9+0+1=10 写 0 进 1
两串都走完了 —— 但 carry 还是 1!
漏 carry → 输出 "000" 正确 → "1000"
2.4 完整代码(Python)
def addStrings(num1: str, num2: str) -> str:
i, j = len(num1) - 1, len(num2) - 1
carry = 0
res_low_to_high = [] # 低位在前,push 顺序天然从个位往高位
while i >= 0 or j >= 0 or carry > 0:
a = ord(num1[i]) - 48 if i >= 0 else 0
b = ord(num2[j]) - 48 if j >= 0 else 0
s = a + b + carry
res_low_to_high.append(s % 10)
carry = 1 if s >= 10 else 0
i -= 1
j -= 1
return ''.join(map(str, reversed(res_low_to_high)))
两个实现细节:
- 数字转值用
ord(c) - 48(或 c - '0'):数字字符 '0' 到 '9' 的 ASCII 码连续,减掉 '0' 的码值 48 就是数值。这是"逐字符转数字",不是"整串转整数" —— 不违反题意。
- 结果用数组 push 再 reverse:竖式天然从低位往高位生成,push 到数组尾部正好是低位在前,最后 reverse 一次 join。别用
unshift/头插 —— 每次 O(n) 的头部搬移,整个算法退化成 O(n²)(见第九节坑 4)。
三、动画怎么看
动画就是一张竖式:num1 行、num2 行右对齐(短串左侧空缺列画成灰虚格),横线下面是结果行 —— 它比上面多一格,
留给最后一次进位长出来的那一位。
- digit 帧:当前列淡金底,
i、j 两根指针分别压在两行刚参与相加的格子上;该侧补零时格子显示灰色的 0;顶上的小 1(进位)标在下一列上方 —— 和你手算时写在竖式顶上的进位一模一样;结果行刚写出的格子橙色描边。
- done 帧:结果行整体变绿,横幅给出答案字符串。
看点有两个:一是
补零格的灰 0 —— "缺位贡献 0,列照算不误";二是
最后溢出格 —— 如果最后一列算完 carry 还是 1,结果行最左那格就会从虚线空格变成 1(把输入换成
"999" 和
"1" 可以看到)。
四、完整推演表
以
"456" + "77" 为主例(maxLen = 3,列号 0 = 最高位 / 2 = 个位):
对照检查三个要点:第 3 帧
j = -1,num2 没有百位,
b 补 0 而列照算;第 2、3 帧
carryIn = 1 接住上一帧的
carryOut;三列算完
carryOut = 0,循环终止 —— 如果最后还有进位,就会多出一帧长出第 4 位。
五、变体:含负数、大数相乘、二进制与链表
5.1 腾讯变体:大数相加含负数
输入可能带负号(如
"-123" +
"45")。解法骨架不变,
外面套一层符号分发:
- 拆符号:先取每个串的符号和绝对值部分(
'-123' → 负,'123');
- 同号:绝对值做竖式加法(就是本题),符号保持;
- 异号:转成大数减法 —— 先比较两个绝对值的大小(先比长度,等长再按字典序逐位比;字符串比大小只能这么比,不能转数字),然后用大的减小的,符号跟随绝对值大的一方。
减法的竖式和加法对称,但有一个加法没有的坑:
借位。
a - b - borrow < 0 时要
+ 10 并把 borrow 置 1;而且结果可能产生
前导零(
"1000" - "999" 逐位算出
"0001"),返回前必须剥掉 —— 只留最后一位,其余 0 全剥(
"000" 要返回
"0" 不是空串)。
面试时先把四种符号组合枚举成一张表再动手(
++/
-- 直接加,
-+/
+- 转减法),这张表就是你的正确性论证,比边写边想稳得多。
5.2 进阶:大数相乘(LC 43)
竖式乘法:
num1 的第
i 位乘
num2 的第
j 位,乘积落在
结果的第 i + j + 1 位(两串各自低位对齐后,
i、
j 从 0 计)—— 这个"下标恰好相加"的规律是整题的题眼,背下来直接写:
def multiply(num1: str, num2: str) -> str:
if num1 == "0" or num2 == "0":
return "0"
m, n = len(num1), len(num2)
res = [0] * (m + n) # 乘积最多 m+n 位
for i in range(m - 1, -1, -1):
for j in range(n - 1, -1, -1):
mul = (ord(num1[i]) - 48) * (ord(num2[j]) - 48)
p1, p2 = i + j, i + j + 1
total = mul + res[p2]
res[p2] = total % 10
res[p1] += total // 10 # 进位往高位累加,之后统一处理
# 剥前导零
k = 0
while k < len(res) - 1 and res[k] == 0:
k += 1
return ''.join(map(str, res[k:]))
注意它和加法的关系:
进位的搬运和竖式加法完全同款(满十进一),只是每列的"加数"从一个变成了
m 个乘积的累加 —— 所以 LC 43 本质上是"LC 415 的每列叠了多层"。
5.3 同族题
家族口诀:
"逐位处理 + 进位/借位变量 + 三连 or 循环条件"就是整个大数运算家族的通用骨架,变的只是 base(67)、存储方向(2)和每列的运算量(43)。
六、Java 版
public String addStrings(String num1, String num2) {
int i = num1.length() - 1, j = num2.length() - 1, carry = 0;
StringBuilder sb = new StringBuilder(); // append 尾插 = 低位在前
while (i >= 0 || j >= 0 || carry > 0) {
int a = i >= 0 ? num1.charAt(i) - '0' : 0;
int b = j >= 0 ? num2.charAt(j) - '0' : 0;
int s = a + b + carry;
sb.append(s % 10);
carry = s >= 10 ? 1 : 0;
i--; j--;
}
return sb.reverse().toString();
}
Java 的
StringBuilder.append 尾插 + 最后
reverse,和 Python 的 push + reversed 完全同构。
七、边界情况
八、复杂度
- 时间 O(max(m, n)):每一列一次加法,列数是较长串的长度(溢出时多一列)。
- 额外空间 O(1):只有
carry 和两个指针;结果串本身是题目要求的输出,不计入(Python 的 reversed + join 是构造输出,不是辅助数据结构)。
对照被排除的做法:转 int / BigInteger 在语言里通常是 O(n²) 甚至更差(大数乘法的复杂度),而且题目明令禁止;
padStart 补零法多 O(m+n) 的拷贝,量级不变但常数和出错面都大。
九、常见坑
9.1 循环条件漏掉 carry
三连 or 少最后一个:
"999" + "1" 输出
"000" 而不是
"1000"。
最高位溢出是这题最经典的丢分点,写完先跑这个用例。
9.2 一侧走完后"跳过"而不是"补 0"
i < 0 时这列 num1 贡献 0,
列照算。写成
if (i >= 0 && j >= 0) 才进循环的话,短串走完整个循环就停了,长串剩余部分和进位全丢。
9.3 字符转数值用错方法
num1[i] 是
字符不是数字:Python 里
num1[i] 得到
'6',直接参与加法是类型错误;正确的是
ord(c) - 48(或
int(c),逐字符转不违反题意)。Java 的
charAt(i) - '0' 同理。另外别忘了题面禁的是"整串转整数"(
Integer.parseInt(num1) 会直接溢出),逐字符转数字恰恰是竖式的本意。
9.4 用 unshift / 头插构造结果
竖式从低位往高位生成,
unshift 每次头插 O(n),整体 O(n²)。
push + 最后 reverse(或 StringBuilder.append + reverse),O(n) 完事。
9.5 结果前导零
本题输入保证无前导零,竖式法也不会产生前导零(多出的那一位只会在 carry 溢出时是 1),但
变体里要当心:减法(含负数变体)和乘法(LC 43)都会算出前导零,返回前必须剥,且剥到只剩一位时要保留
"0"。
9.6 负数变体里直接照抄减 48
输入带负号时,
'-123'[0] 是
'-',
ord('-') - 48 = -3 —— 静默算出一个负"数字",答案错得莫名其妙。变体必须
先拆符号再进竖式(见 5.1),这也是面试官加负号这条件的全部用意:考你会不会在"纯算法"外面包一层工程上的输入处理。
十、面试怎么答
- 一句话定性:"不能转整数就是把捷径排除,剩下的是列竖式 —— 逐位加 + 进位变量,我直接写。"
- 先说三个实现要点再动手:末尾对齐下标补零、每列加-写-进、循环条件三连 or —— 第三点主动说出来,是这题最值钱的一句。
- 主动提溢出用例:
"999" + "1" → "1000",说明你意识到 carry 会长出一位。
- 复杂度:O(max(m, n)) 时间,O(1) 额外空间。
- 延伸准备好:含负数(符号分发表 + 减法借位 + 剥前导零)、大数相乘(
i + j + 1 落位)—— 腾讯面试从这题出发往下追问基本就是这两条路。
结语
字符串相加表面是模拟,内核是
"把一个你早就会的过程翻译成无遗漏的代码"。竖式人人会列,但三连 or、补零、reverse 这三处细节决定了翻译的完整度 —— 算法的能力边界往往不在"想到",而在"写全"。下一题若被追问大数相乘,记住那个落位公式:
i + j + 1。