ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

手撕大数加法:从字节面试题到工程思维的本质剖析

手撕大数加法:从字节面试题到工程思维的本质剖析 前几天我一个朋友面字节的算法岗走出面试间后第一句话不是吐槽难度而是说你知道么面试官让我在白板上写一个大数加法。我这个朋友编程基础不算弱八股文背了一堆结果在这道看起来简单的手撕题上接二连三踩坑。说实话字节面试的手撕环节考大数加法并不是什么新鲜事但很多候选人跟他的反应一样——这题不是 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 遇到非数字字符会抛ValueErrorJava 和 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 的反转与 toStringJava 版本的核心是不要用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) { stackint 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(nm)。我建议你在准备大数加法的时候顺便把大数乘法的手撕模板也过一遍。因为加法是大数运算的底座乘法在加法的外面包了一层双重循环理解了加法再看乘法会轻松很多。字节面经里也经常出现先写加法再顺势问乘法的组合拳。6. 过来人的复盘经验手撕大数加法拼的不是手速6.1 先讲思路再动手是手撕题最划算的投入我在身边朋友的面经里看到过太多反面例子拿到题不到十秒就开始写代码写得飞快结果写到一半发现自己没考虑负数又把整个函数推倒重写。白板上改来改去心态一崩后面就全乱了。手撕题不是打字比赛。面试官给你题目的时候脑子里已经开始计时了但这个计时器更看重你从理解到落地的效率而不是你的第一行代码出现在第几秒。先用一分钟时间把思路讲清楚说我打算从低位到高位逐位加用 carry 保存进位最后处理最高位进位面试官点头之后再动手反而会让对方觉得你是一个思维清晰的人。6.2 用极端用例当开场白把主动权握在自己手里还有一个很有用的习惯写代码之前先口述一两个测试用例特别是999 1。这不只是在验证你的算法也是在暗示面试官你很清楚这道题的难点在哪。很多面试官听到你主动说极端用例就不会再额外挖边界问题了因为你们已经达成了共识。写完代码后一定要把你说过的用例跑一遍。有的候选人白板代码本身是对的但他不演示面试官就不知道他是真懂还是碰巧写对。你主动把999 1的每一步进位过程讲出来这个印象分会非常实。6.3 失分点清单每次面试前用 30 秒过一遍根据我自己的复盘大数加法的失分点其实高度集中在几个固定位置。每次面试前我都会建议朋友在脑子里过一遍这张清单。字符转数字前是否用了-0或ord转换。循环条件里是否漏了carry ! 0。低位结果顺序是否反了最后有没有反转或者用栈修正。两个字符串长度不等时短字符串访问是否导致越界。是否主动澄清了负数、小数、空串、前导零这些输入问题。是否脱口而出时间复杂度和空间复杂度。是否提到了不允许使用内置大数库这个前提。这七条看着简单实际上每条都能单独成为挂掉一场面试的原因。尤其是前两条我见过不少候选人代码只差这两个字符但面试结束时也没发现。我自己后来帮团队做面试评审也会用大数加法来筛候选人。我通常不会在代码正确性上卡人我更关注的是他遇到问题时的反应以及他能不能把思路讲给我听。字节面试官大多也是同样的心态手撕的目的不是造一台只会做加法的机器而是找到那个能在大数加法的细小世界里把问题边界、代码逻辑和异常处理都想得很透彻的人。如果你能把今天这些经验真正内化成自己的写码习惯下次再遇到手撕大数加法这六个字就不会紧张只会觉得机会来了。
返回列表