前几天,我一个朋友面字节的算法岗,走出面试间后第一句话不是吐槽难度,而是说:你知道么,面试官让我在白板上写一个大数加法。我这个朋友编程基础不算弱,八股文背了一堆,结果在这道"看起来简单"的手撕题上接二连三踩坑。说实话,字节面试的手撕环节考大数加法并不是什么新鲜事,但很多候选人跟他的反应一样——这题不是 LeetCode 415 吗?不是挺基础的吗?可真正到了白板上,从边界条件到代码实现,每一步都可能让你栽跟头。
我后来复盘了一下他整个面试过程,也翻了不少面经,发现大数加法这道题之所以被反复拿出来当手撕题,恰恰是因为它足够基础,基础到能暴露你的工程直觉。这篇文章就围绕这道题,把我自己和身边人踩过的坑、总结出来的经验完整梳理一遍。不管你是准备字节的算法面、后端面,还是纯粹想把字符串大数运算写扎实,这篇内容都能直接拿来用。
1. 为什么字节面试离不开这道题:手撕大数加法到底在考什么
1.1 一道题同时探测四个能力
面试官拿大数加法出来,绝不只是为了看你会不会写两个字符串相加。这种题的门槛低到几乎所有人都能聊两句,但正因为门槛低,它才可以同时考察四件事。
第一是基本功扎不扎实。字符串怎么遍历、字符怎么转数字、进位怎么保存,都是最底层的语言操作。很多人平时写业务代码习惯了调库里的大数类型或 API,一上手就露馅,连'0'和数字0之间的 ASCII 差都会搞混。
第二是边界条件的敏感度。空字符串、一个数比另一个数长很多、最高位加完还有进位、输入带前导零、字符串里出现非数字字符,这些情况每一条都是潜在的失分点。能主动说出这些边界,而不是等面试官提醒,这是区分"背过题"和"真会做"的关键。
第三是复杂度意识。你会不会主动说时间复杂度和空间复杂度都是 O(n),而不是写完代码就完了。面试官追问"能不能优化空间"时,你能不能给出不用额外数组的写法。
第四是沟通习惯。手撕题最忌讳闷头写。候选人拿到题以后是先澄清输入范围,还是直接开写,这比代码本身更能反映真实的工作方式。面试官想要的是一个合作者,不是一个单机做题家。
1.2 面试官期待的正确打开方式
我先说说大多数人在手撕环节挂掉的原因,不是代码写得不对,而是节奏不对。一个理想的大数加法流程应该是这样的:
先确认需求。问清楚输入是字符串还是数组,允不允许负数,有没有可能是空串,字符集是不是只有 0 到 9。这些问题不是多余,而是展示你的需求分析习惯。
然后口头举例。拿999 + 1这种极端例子先走一遍,明确进位会一直往上传,最后多出一位变成1000。这一步是在告诉面试官:我理解了问题,而且我知道坑在哪里。
再写代码。写的过程中边写边说每个关键步骤是干什么的,而不是写完再解释。最后主动跑几个测试用例,包括"0" + "0"、"123" + "456"、"999" + "1"、"1" + "999"。
这套流程本身比代码更值钱。因为你工作以后解决的问题,大多数不是"聪明地想出算法",而是"把需求边界理清楚、把异常情况处理干净"。字节的面试官大多也是这个出发点,他们要的是能一起打仗的人。
2. 从竖式加法到代码:把小学二年级的算法翻译成程序
2.1 竖式加法的本质:低位对齐 + 逐位求和 + 向上进位
大数加法的核心逻辑,说白了就是我们小学列竖式做加法的那套流程。两个很大的数字,因为超过了语言内置整数类型的表示范围,所以不能用int直接相加。我们需要把它们拆成十进制位,从个位开始逐位相加,每一位的和超过 9 就把十位部分进到上一位去。
计算机里怎么模拟这套流程?把两个数分别当作字符串,从字符串的末尾开始往前扫。为什么从末尾而不是从开头?因为竖式加法必须从低位开始,这样才能处理进位。如果你从高位开始加,等处理到低位发现进位了,回头改高位的结果,代码就复杂了。
还有一个容易被忽视的点:字符'9'对应的 ASCII 值是 57,数字9就是 9。你直接把'9'加'1'得到的是字符而不是数字,所以在做加法前必须做一次转换:num[i] - '0'。这是所有字符串数字运算的基础,也是很多人写的时候一紧张就写错的地方。
2.2 第一版最直观的实现:反转字符串 + 统一长度
为了把逻辑讲清楚,先看一个最容易理解的版本,用 Python 写,思路是先把两个字符串反转,让下标 0 变成个位,然后按位遍历,缺的位当 0 处理。
def addStrings(num1: str, num2: str) -> str: a = num1[::-1] b = num2[::-1] n = max(len(a), len(b)) carry = 0 res = [] for i in range(n): x = int(a[i]) if i < len(a) else 0 y = int(b[i]) if i < len(b) else 0 s = x + y + carry carry = s // 10 res.append(str(s % 10)) if carry: res.append(str(carry)) return ''.join(res[::-1])这段代码虽然多了一次字符串反转,但逻辑非常清晰。a[i]和b[i]永远是从个位开始的第 i 位,长度不一致时用 0 补齐,进位变量carry在每一位计算时都加上去。最后循环结束如果carry还等于 1,说明最高位产生了一个新进位,比如999 + 1,需要在结果前面补一个"1"。
2.3 为什么很多最优写法不反转,而是从末尾倒着遍历
反转字符串的写法好理解,但面试官通常会追问:能不能不反转,直接从后往前处理?字节的手撕题很多时候要求你在白板上写更优雅的版本。
从后往前扫描的核心思路是:用两个指针i和j分别指向num1和num2的末尾,每轮计算一个位置,然后i--、j--,直到两个指针都小于 0 并且没有进位。
这种写法不需要额外的反转数组,代码量也更少。但代价是你在循环条件里必须同时判断三个东西:i >= 0 || j >= 0 || carry != 0。很多人栽在这里,只判断了i >= 0 || j >= 0,最后一位的进位直接丢掉了。
我个人建议,新手入门先用反转版本理解原理,面试写代码时用双指针版本,但一定要留出时间把carry != 0这个条件写完整。下面的实现章节会给 C++、Java、Python 三个版本的具体代码,全部采用双指针倒序遍历。
3. 真正容易让人写挂的小细节:一行代码毁掉整场面试
3.1 最高位进位:999 + 1 是永不过时的测试用例
很多人在白板上写大数加法,写到while (i >= 0 || j >= 0)就停了,觉得循环结束后就完事了。然后面试官补一句:那"999" + "1"是什么结果?你才反应过来,循环结束的时候carry还是 1,需要再往结果里补一个字符。
这个错误太经典了。999 + 1看起来极端,但它恰好卡在每一位都要进位的场景上。个位进到十位,十位进到百位,百位进到千位,最后一个进位必须体现在新的一位上。处理方式就是在循环条件里加上|| carry != 0,或者在循环结束后判断一次carry > 0。
我有一个习惯,不管题目怎么变,写完加法后第一个测试用例永远是999 + 1,第二个是1 + 999,第三个是0 + 0。这三个用例能把进位传播、顺序反转、空结果路径全部覆盖到。
3.2 长度不一致时下标越界:三种语言的写法差异
字符串长度不一样时,比如"123456" + "7",短的字符串指针会先变成负数。代码里处理的方式通常有两种。
一种是在循环体里对下标做保护:
x = int(num1[i]) if i >= 0 else 0 y = int(num2[j]) if j >= 0 else 0另一种是把短的字符串在循环前用0补齐到和长的一样长。第二种写法逻辑上更直白,但会引入额外空间。手撕题的代码最好写成第一种,边遍历边判断,不额外分配空间。
很多人在 C/C++ 里写num1[i--] - '0',一旦i已经小于 0 还继续访问,就是未定义行为,白板上看不出来,但面试官会立刻指出问题。Java 里则是StringIndexOutOfBoundsException。所以先判断i >= 0,再取值,这个顺序不能乱。
3.3 前导零、空串和非法字符怎么交代
题目如果允许"000123"这样的输入,你的算法其实天然能处理,因为按位加法会老老实实算出"000124"而不是"124"。那要不要去掉前导零?这取决于你最初和面试官确认的需求。如果只说"两个非负整数字符串相加",我建议你主动提一句:输入有没有前导零?输出要不要保留?
至于空串,在 LeetCode 的题目约束里基本不会出现,但面试的时候候选人不该默默假设输入一定合法。正确的做法是在开局澄清阶段就问清楚:输入是否保证只含数字?是否保证非空?如果面试官说"你自己定义接口",那么你写出的代码就必须对空串有明确处理,比如约定空串视为"0"。
非数字字符就更有意思了。有些面试官会在这个问题上挖坑,问你如果字符串里混入了"-"或者其他字符怎么办。这说明题目已经从单纯的大数加法升级成了"带格式校验的大数加法"。如果你在代码里用int(num1[i])直接转换,Python 遇到非数字字符会抛ValueError,Java 和 C++ 也各有各的麻烦。答案不是每种语言都写一遍健壮解析,而是先说明当前版本假设输入已经经过合法性校验,如果确实需要容错,可以在入口处增加预检。
3.4 别在面试里直接调用 BigInteger、BigDecimal 这类大数库
这是手撕题的大忌。面经里刷到过不少反面案例:候选人一看题目是大数加法,直接在 Java 里写new BigInteger(num1).add(new BigInteger(num2)).toString(),代码两行搞定,结果面试官脸都绿了。
不是说你不能用,也不是说这代码有错,而是手撕题的核心目的是考察你手写字符串运算的能力。一旦调用大数库,你等于把题目最想考察的那部分外包给了标准库。有些面试官会追加一句:如果不准用内置大数类型呢?这时候你就必须老老实实把竖式加法写出来。
但反过来想,这个例子也提醒我们,大数运算的真实工程场景里,标准库确实是最可靠的方案,没必要重复造轮子。只是面试场景有它自己的游戏规则,你至少要让面试官看到你具备从零实现的能力,再去谈工程优化。
4. C++、Java、Python 三种实现:从代码风格看出工程习惯
4.1 C++ 实现:string 加 reverse 是主流姿势
字节面试如果选了 C++,那手撕大数加法基本逃不开std::string和std::reverse。C++ 的性能意识比较强,所以写出来的代码往往要考虑返回值和拷贝。
#include <string> #include <algorithm> using namespace std; string addStrings(string num1, string num2) { string res; int i = num1.size() - 1; int j = num2.size() - 1; int carry = 0; while (i >= 0 || j >= 0 || carry != 0) { int sum = carry; if (i >= 0) { sum += num1[i--] - '0'; } if (j >= 0) { sum += num2[j--] - '0'; } carry = sum / 10; res.push_back('0' + sum % 10); } reverse(res.begin(), res.end()); return res; }这段代码有两个细节值得在面试时主动讲出来。第一是num1[i--] - '0'利用了后缀自减,在取完当前字符后立刻移动指针,代码简洁,但如果面试官不熟悉这种写法,建议拆成两行,清晰优先。第二是用push_back而不是res +=,因为char类型的追加在string上语义明确,也避免产生不必要的临时对象。
4.2 Java 实现:StringBuilder 的反转与 toString
Java 版本的核心是不要用String直接做字符拼接,因为字符串是不可变对象,每次+都会创建新对象。面试时写StringBuilder是基本功,也让面试官知道你在意内存分配。
public String addStrings(String num1, String num2) { int i = num1.length() - 1; int j = num2.length() - 1; int carry = 0; StringBuilder sb = new StringBuilder(); while (i >= 0 || j >= 0 || carry != 0) { int sum = carry; if (i >= 0) { sum += num1.charAt(i--) - '0'; } if (j >= 0) { sum += num2.charAt(j--) - '0'; } carry = sum / 10; sb.append((char) ('0' + sum % 10)); } return sb.reverse().toString(); }很多人会好奇,为什么先往StringBuilder里追加低位,最后再reverse,而不是用一个prepend或者insert(0, ...)。原因很简单,StringBuilder的append是在末尾追加,底层char[]是连续内存,性能好。insert(0, ...)涉及所有已有元素的搬移,复杂度恶化到 O(n^2)。这里体现的其实是一个通用原则:优先在序列尾部操作,最后统一反转。
4.3 Python 实现:明明有 int 上限,为什么还要自己模拟
Python 的整数理论上是无限精度的,你写int(num1) + int(num2),结果完全正确。但面试官让你手撕大数加法,赌的就是你不会直接这么干。正确的面试姿势是写出字符串模拟版本,把大数运算的底层逻辑展示出来。
def addStrings(num1: str, num2: str) -> str: i, j = len(num1) - 1, len(num2) - 1 carry = 0 res = [] while i >= 0 or j >= 0 or carry: a = int(num1[i]) if i >= 0 else 0 b = int(num2[j]) if j >= 0 else 0 s = a + b + carry carry = s // 10 res.append(str(s % 10)) i -= 1 j -= 1 return ''.join(reversed(res))Python 版写起来最省事,但有一个最容易忽略的点:int(num1[i])看起来简单,背后做的是字符到整数的转换,比ord(num1[i]) - ord('0')的写法多了一层函数调用开销。不过面试场景不追求极致性能,但你要能说出这两者的区别,就显得你真的懂 Python 的字符处理。
4.4 时间复杂度与空间复杂度这笔账怎么算
不管哪个语言版本,核心循环都只遍历较长的那个数字字符串一次,所以时间复杂度是 O(max(m, n)),m 和 n 分别是两个输入的长度。空间复杂度基本是 O(max(m, n)),因为你要保存结果字符串。
如果面试官追问能不能 O(1) 额外空间,那就建议在原字符串上直接修改,把较长的那个字符串当作结果容器。但因为两个字符串的长度不一定相同,实际工程里很少这么写,面试里答出来就是亮点。真正的加分答案是先老老实实说 O(n) 版本,再补充一句:如果输入是可变的字符数组,且允许原地修改,可以把结果写回较长的数组,把空间复杂度压到 O(1)。
另一个常见的复杂度陷阱是:不要写出每次往结果头部插入字符的代码,那是 O(n^2),面试官一眼就能看出来。
5. 基础版写完后,面试官的追问才是真正的加权题
5.1 负数怎么办:把加法问题扩展成带符号运算
大数加法之后最顺理成章的追问就是:如果输入可能是负数呢?这时候不能直接套用上面的竖式加法,因为进位的方向和借位的方向完全不同。
我的建议是把问题拆成四步。第一步,判断两个数的符号,确定最终结果的符号。如果两个数同号,就是绝对值相加,符号跟随原数。如果异号,就变成绝对值相减,结果的符号跟随绝对值大的那个数。
第二步,实现绝对值比较函数,判断abs(num1)是否大于abs(num2)。
第三步,实现绝对值大减小的大数减法,核心是从低位往高位逐位相减,不够减就向高位借 1,借位的效果是当前位加 10,高位减 1。
第四步,根据符号组合输出结果,并且去掉高位多余的 0,特别要注意结果是 0 的时候要输出"0"而不是空串或"-0"。
这个追问考察的不是你会不会写减法,而是你有没有把问题拆解成可复用模块的意识。吸一口,面试官能问的东西就一下多出来了:大数减法、大数乘法甚至大数除法都能顺着展开。
5.2 带小数部分怎么办:先按小数点拆分,再分别处理
大数加法的另一个常见变体是处理小数,比如"123.456" + "0.789"。逻辑上不复杂,但需要先把整数部分和小数部分拆开。
整数部分沿用普通大数加法,小数部分要注意对齐小数点,也就是把小数位少的那个数补 0 到同样长度。这里有一个很关键的点:小数部分相加后产生的进位要传递给整数部分。比如"0.999" + "0.001",小数位是999 + 001 = 1000,需要保留三位小数结果000,同时向整数部分进位 1,最后结果是"1.000"。如果你只盯着整数加法,这个进位就丢了。
这种例子我在实际系统中的确遇到过。很多金融场景里金额运算不允许用浮点数,就是因为二进制浮点数没法精确表示十进制小数,只能用字符串或者定点数来算。面试官如果从这里继续深挖,其实是在考察你的工程场景敏感度。
5.3 不让用反转怎么办:用栈实现从低位到高位的顺序
有些面试官会故意提限制,说不能对字符串做反转,也不希望用reverse函数,要求保持原始字符串不变。这时候你仍然可以从后往前遍历,把每一位的中间结果先存到栈里,最后依次弹出。
string addStrings(string num1, string num2) { stack<int> st; int i = num1.size() - 1, j = num2.size() - 1, carry = 0; while (i >= 0 || j >= 0 || carry != 0) { int sum = carry; if (i >= 0) sum += num1[i--] - '0'; if (j >= 0) sum += num2[j--] - '0'; carry = sum / 10; st.push(sum % 10); } string res; while (!st.empty()) { res.push_back('0' + st.top()); st.pop(); } return res; }要是没有栈也不能反转,还有一个办法:先算出结果的长度,从最后一个位置往前填,填完再把字符串头尾对调。本质上还是在模拟数组从后往前写,思路都是一样的。面试中你只要能说出"用栈消除反转"这层意思,就已经比大部分候选人强了。
5.4 如果字符串有几个亿的长度,内存放不下怎么办
最后一个大杀器级追问是:如果输入字符串非常长,比如单条就有好几 GB,没法一次性读进内存,怎么写?
我头一次听到这个问题时愣了一下,后来想清楚,这是从单机面试题跳到分布式工程的信号。答案不需要你真的去写一个分布式框架,而是表达分治思想:把超长字符串按固定长度切块,从最后一块开始往前处理,每块内部做普通的大数加法,然后把这一块产生的进位传到前一块。
一个简单的分块做法是把数字按 9 位或者 18 位切成一段。为什么按这些位数切?因为 Java 的int最多安全表示 9 位十进制数,long最多安全表示 18 位十进制数,这样每一块相加时结果不会溢出,进位跨块也好处理。如果更进一步,每块数据可以放到不同的计算节点上,从尾部往头部逐块聚合进位,这就是 MapReduce 思路的雏形。
这个问题的价值不在于你真的会去手写一个分布式加法器,而在于它考察你面对"数据规模超出直觉"时的第一反应。很多人一听到几个 GB 就慌了,其实回到根本,还是竖式加法,只是把"位"换成了"块",把"进位"换成了"跨块进位"。
5.5 顺着大数加法还能挖出大数乘法
手撕环节如果时间充裕,面试官可能在大数加法的基础上让你顺手写个大数乘法。大数乘法的基本逻辑是:num1的每一位和num2的每一位相乘,把结果累加到对应的偏移位置上,最后统一处理进位。这样时间复杂度是 O(n*m),空间复杂度是 O(n+m)。
我建议你在准备大数加法的时候顺便把大数乘法的手撕模板也过一遍。因为加法是大数运算的底座,乘法在加法的外面包了一层双重循环,理解了加法再看乘法会轻松很多。字节面经里也经常出现"先写加法再顺势问乘法"的组合拳。
6. 过来人的复盘经验:手撕大数加法,拼的不是手速
6.1 先讲思路再动手,是手撕题最划算的投入
我在身边朋友的面经里看到过太多反面例子:拿到题不到十秒就开始写代码,写得飞快,结果写到一半发现自己没考虑负数,又把整个函数推倒重写。白板上改来改去,心态一崩,后面就全乱了。
手撕题不是打字比赛。面试官给你题目的时候,脑子里已经开始计时了,但这个计时器更看重你从理解到落地的效率,而不是你的第一行代码出现在第几秒。先用一分钟时间把思路讲清楚,说"我打算从低位到高位逐位加,用 carry 保存进位,最后处理最高位进位",面试官点头之后再动手,反而会让对方觉得你是一个思维清晰的人。
6.2 用极端用例当开场白,把主动权握在自己手里
还有一个很有用的习惯:写代码之前先口述一两个测试用例,特别是999 + 1。这不只是在验证你的算法,也是在暗示面试官,你很清楚这道题的难点在哪。很多面试官听到你主动说极端用例,就不会再额外挖边界问题了,因为你们已经达成了共识。
写完代码后,一定要把你说过的用例跑一遍。有的候选人白板代码本身是对的,但他不演示,面试官就不知道他是真懂还是碰巧写对。你主动把"999" + "1"的每一步进位过程讲出来,这个印象分会非常实。
6.3 失分点清单:每次面试前用 30 秒过一遍
根据我自己的复盘,大数加法的失分点其实高度集中在几个固定位置。每次面试前,我都会建议朋友在脑子里过一遍这张清单。
- 字符转数字前是否用了
-'0'或ord转换。 - 循环条件里是否漏了
carry != 0。 - 低位结果顺序是否反了,最后有没有反转或者用栈修正。
- 两个字符串长度不等时,短字符串访问是否导致越界。
- 是否主动澄清了负数、小数、空串、前导零这些输入问题。
- 是否脱口而出时间复杂度和空间复杂度。
- 是否提到了不允许使用内置大数库这个前提。
这七条看着简单,实际上每条都能单独成为挂掉一场面试的原因。尤其是前两条,我见过不少候选人代码只差这两个字符,但面试结束时也没发现。
我自己后来帮团队做面试评审,也会用大数加法来筛候选人。我通常不会在代码正确性上卡人,我更关注的是他遇到问题时的反应,以及他能不能把思路讲给我听。字节面试官大多也是同样的心态:手撕的目的不是造一台只会做加法的机器,而是找到那个能在大数加法的细小世界里,把问题边界、代码逻辑和异常处理都想得很透彻的人。如果你能把今天这些经验真正内化成自己的写码习惯,下次再遇到"手撕大数加法"这六个字,就不会紧张,只会觉得机会来了。