
力扣第66题题名叫“加一”。我第一次看到这名字下意识以为跟手机品牌有点关系点进去才反应过来这是一道经典的数组模拟加法题。这道题在力扣热题100里经常露脸也是各家力扣刷题攻略里公认的入门必做题。难度标签是“简单”但简单不等于没东西里面藏的边界条件、数组处理思路足够让不少人栽跟头。这篇文章就从题目本身开始把解题思路、代码实现、边界测试、常见坑位一次讲透顺便聊聊这类数组题背后的通用套路。不管你是刚刷题的新手还是准备面试想快速过一遍经典题的选手这篇笔记应该都能给你一些实在的东西。1. 先看懂题目加一到底是什么问题1.1 原题复述与直观理解题目原文大致是这样的给定一个由整数组成的非空数组所表示的非负整数在该数的基础上加一。最高位数字存放在数组的首位数组中每个元素只存储单个数字假设除了整数0之外这个整数不会以零开头。什么意思呢就是说给你一个数组比如[1, 2, 3]它表示的不是一个长度为3的数组而是数字123。你要在这个数字的基础上加1得到124然后把它拆成一位一位的数字返回[1, 2, 4]。再举几个例子输入[4, 3, 2, 1]表示4321加一后是4322输出[4, 3, 2, 2]。输入[9]表示9加一后是10但数组每一位只能存一个数字所以输出[1, 0]。输入[9, 9, 9]表示999加一后是1000输出[1, 0, 0, 0]。理解这道题第一件事就是把数组和数字对应起来。数组是从左到右表示高位到低位跟日常写数字的习惯一致。很多人上来会想把数组转成整数直接加一但这里有个大坑数组可能很长比如几十位甚至上百位早就超过了语言整数类型的表示范围。题目用数组存储本身就是想让你用数组模拟加法而不是偷懒转int。1.2 考的是数组不是加法这道题表面看是加法实际上考的是对数组下标和进位的理解。真正的加法逻辑非常简单难点在9这个数字身上。只有9加一才会变成10需要进位如果某一位原本是9又收到了低位的进位它也要变成0并且继续往高位传。如果是全9的情况比如999加一变成1000数字位数增加了一位数组长度也要跟着变。所以这道题真正想考察的能力可以拆成三点你能不能把“数字加一”转换成“数组逐位处理”。你能不能正确处理进位传递尤其是连续进位。你能不能处理数组扩容这种边界情况。这三点恰好对应了很多数组类题目的通用考点。你会发现这道题虽然代码短但把数组题常见的麻烦事都浓缩进去了。2. 常规解法拆解从模拟加法到代码落地2.1 从最后一位开始做进位处理思路是从数组末尾开始往前遍历。初始时由于题目就是加一我们可以设定一个进位carry初始为1代表要加上的那个1。在每一位上当前位的值加上carry得到sum。如果sum小于10说明没有进位这一位更新为sum后面所有位都不用变直接返回结果如果sum等于10说明有进位这一位变成0carry保持1继续处理前一位。另一种等价写法是在第i位时直接给digits[i]加1然后判断digits[i]是否等于10。如果等于10说明需要进位把当前位改成0进位继续往前如果不等于10直接返回。这个写法更贴近题目给的“加一”动作理解起来更舒服。我用[1, 9, 9]推演一遍第二种写法初始数组[1, 9, 9]。从下标2开始digits[2] 9加一后变成10等于10置为0继续往前。到下标1digits[1] 9加一后变成10等于10置为0继续往前。到下标0digits[0] 1加一后变成2不等于10直接返回[2, 0, 0]。这个推演过程非常清晰你会发现进位就像是多米诺骨牌从右往左一路倒过去直到遇到一个不是9的数字才停下来。2.2 为什么先加1再判断等于10很多初学的人会问为什么不先判断数字是不是9再加其实两种方式都可以。先加再判断的好处是逻辑统一不用区分当前位是不是9都是先加一再看结果。比如当前位是8加一变成9不等于10直接返回。如果是9加一变成10置为0继续进位代码只需要一个分支。这种“先行动后判断”的写法在处理边界时更自然因为如果当前位加一后是10那它原来一定是9而9只有一种归宿就是变成0并进位。判断条件写成digits[i] 10比较直观。当然也有人喜欢写if digits[i] 1 9本质是一样的只是多一次加法运算个人风格问题而已。另外还有一个点值得注意我们之所以从右往左遍历是因为加法进位方向就是自右向左。如果从前往后遍历你根本不知道低位会不会向当前位进位这是很多新手写错的核心原因。2.3 代码实现Python、Java、JavaScript这里给出三种常见语言的实现逻辑完全一致只是语法风格不同。Python版本def plusOne(digits): for i in range(len(digits) - 1, -1, -1): digits[i] 1 if digits[i] 10: digits[i] 0 else: return digits return [1] [0] * len(digits)Java版本class Solution { public int[] plusOne(int[] digits) { for (int i digits.length - 1; i 0; i--) { if (digits[i] 9) { digits[i]; return digits; } digits[i] 0; } int[] ans new int[digits.length 1]; ans[0] 1; return ans; } }JavaScript版本var plusOne function(digits) { for (let i digits.length - 1; i 0; i--) { if (digits[i] 9) { digits[i]; return digits; } digits[i] 0; } return [1, ...digits]; };上面三份代码Python用“加一后判断”Java和JS用“小于9判断”。原因是我平时写Python喜欢少写分支而Java和JS这样写能省一次加法和比较。你选一种自己顺手的记就好。还有一种更通用的写法引入carry变量适合扩展到其他加法类题目def plusOne(digits): carry 1 for i in range(len(digits) - 1, -1, -1): total digits[i] carry digits[i] total % 10 carry total // 10 if carry 0: return digits return [1] [0] * len(digits)这个版本用total % 10得到当前位的值用total // 10得到进位。它比前面的写法稍显繁琐但模板化程度更高刷到“字符串相加”“二进制求和”的时候可以直接平移。我建议你两种写法都动手敲一遍感受一下区别。3. 边界条件与测试用例这题大多数坑都在边角3.1 关键测试用例整理拿到题目先别急着写代码先在脑子里过一遍测试用例。常规用例是[1, 2, 3]变[1, 2, 4]这个大多数人都能过。真正决定代码对错的是下面这些边角输入期望输出说明[9][1, 0]个位9加一位数增加[9, 9][1, 0, 0]全部是9进位两次并增加一位[1, 9][2, 0]低位进位高位不受影响[9, 8][9, 9]低位不是9只改当前位[0][1]最小非负整数0加一[9, 9, 8][9, 9, 9]中间遇到非9就停止进位这些用例覆盖了无进位、一次进位、连续进位、全9进位后数组扩容。把这几类测完代码基本就稳了。尤其是[1, 9]这种很多人写的时候直接从头遍历遇到9就置零结果把1也变成0了这就是没理解进位方向。3.2 全9场景的原因与处理当数组里全部是9比如[9, 9, 9]加一后变成1000位数从3位变成4位。这时如果仍然在原数组上操作你会发现循环结束后所有位都变成了0没法表示那个多出来的1。所以要新建一个长度加一的数组首位放1后面全部是0。这里有个大家容易忽略的点只有所有位都是9时才会走到循环结束还没返回。只要有一位不是9在那一位加一后就会直接return。所以最后返回新数组这个动作天然就只属于“全9”的情况逻辑上不需要额外用flag标记。从数学上看更清楚如果一个数是10^n - 1也就是n位全是9那么加一后恰好是10^n在数组里就是1后面跟n个0。这跟进制转换里的“进位溢出”是一回事。理解了这个原理你就能明白为什么代码最后一行写的是[1] [0] * len(digits)而不是[1] [0] * (len(digits) - 1)。4. 复杂度分析与优化空间4.1 时间与空间复杂度推算时间复杂度循环从最后一位开始扫描最好情况是个位不是9比如[1, 2, 3]只扫描一位就返回时间复杂度 O(1)。最坏情况是全部是9要扫描整个数组时间复杂度 O(n)。面试里一般说最坏 O(n) 比较稳妥。但如果你深挖一层平均时间复杂度其实接近 O(1)。为什么因为每个数字是0到9均匀分布的话个位是9的概率只有1/10。遇到个位是9才需要看十位十位也是9的概率是1/100以此类推。期望扫描的位数是1 1/10 1/100 ... 10/9这是一个常数所以在随机数据下平均耗时非常短。这个点面试时主动说出来会显得你不仅会写代码还懂分析和估算。空间复杂度除了全9情况需要创建新数组长度n1空间 O(n)其他情况在原数组上修改空间 O(1)。如果面试官要求原地修改记得说明你的代码已经是在原数组上操作了修改了传入的参数数组本身。4.2 能优化吗可以不创建新数组吗这个问题常被追问。全9时数字位数增加存储空间必然要多一位所以不创建新数组是不可能的。但在非全9场景下可以不创建新数组直接在原digits数组上修改。上面给的代码就是这个思路。另外一个常被问的点是能不能从前往后遍历答案是不能。因为加法进位是自右向左传递的从前往后处理的话你无法预知低位是否会向当前位进位。除非你先扫描一遍确认有没有连续9的区间但那样反而多了一次遍历并不划算。还有一种情况是面试官问你“如果禁止修改原数组怎么办”。那你可以先复制一份数组再操作空间复杂度就变成 O(n) 了。虽然空间变多但避免了副作用。工程上到底选哪种取决于你调用函数之后还想不想继续用原数组。刷题时默认允许修改所以大部分题解都是原地改。5. 常见错误与调试记录5.1 常见错误速查表我自己刷这道题的时候包括给朋友讲题时发现错误主要集中在这几类整理成一张表错误类型错误代码示例问题原因正确做法从前往后遍历for i in range(len(digits))低位的进位还没算高位先改了必须从后往前倒序遍历循环结束后忘记返回新数组只改digits[0] 1全9场景数组长度不够return [1] [0] * n用int转换int(.join(map(str, digits))) 1大数溢出/精度丢失用数组模拟加法把9直接变0但不进位遇到9就置0并break遗漏连续进位继续往前扫描返回类型不对返回字符串或整数题目要求返回数组保持和输入一样是数组这几个错误里最隐蔽的是“从前往后遍历”。比如[9, 8]从前往后走第一位9加一变成10你把它置0然后 break结果返回[0, 8]错得离谱。正确的结果应该是[9, 9]因为8加一后不需要进位9根本不用动。这个例子充分说明遍历方向的错误不是靠调代码能发现的要从思路上纠正。5.2 我用过的调试技巧调试这道题最推荐的方法是把测试用例切成几个类别逐个打断点。我习惯写一个print语句把每次循环后的digits打出来。比如输入[9, 9]第二次循环后打印出来的是[0, 0]走到最后返回[1, 0, 0]你一眼就能看出进位有没有正确传递。另一个技巧是写一个随机测试脚本生成10到20位的随机数转成数组调用你的函数再转回数字加一核对。这个方法在验证边界时非常好用比自己干想用例强很多。import random def plusOne(digits): for i in range(len(digits) - 1, -1, -1): if digits[i] 9: digits[i] 1 return digits digits[i] 0 return [1] [0] * len(digits) for _ in range(1000): n random.randint(0, 10 ** 20) digits list(map(int, str(n))) expected list(map(int, str(n 1))) res plusOne(digits) if res ! expected: print(错误输入:, digits) print(错误输出:, res) print(期望输出:, expected) break else: print(随机测试全部通过)这段脚本能覆盖大量随机情况尤其是中段进位和全9进位。如果你想更变态一点可以专门构造[9] * 100这种超长全9数组测一下扩容逻辑是否稳。6. 从第66题延伸刷题思路与后续扩展6.1 类似题目与变体加一这道题虽然简单但它的变体不少。比如“字符串相加”其实就是多位数字字符串的加法处理思路同样是逐位相加加进位。还有“二进制求和”也是同一个套路只不过进制从10变成2进位条件从10变成2。我建议按这个顺序往后刷题目核心考点与加一的关联67. 二进制求和字符串进位进制2把加一的进位模板扩展到任意进制415. 字符串相加字符串大数加法不同长度综合处理两个数字的逐位加法2. 两数相加链表存储数字逐位加法数据结构换成链表思路完全一样这几道题做完你会对“进位”这个概念形成肌肉记忆以后再遇到大数加法类的题脑子里直接就有模板。这里给你一个通用的字符串相加模板def addStrings(num1, num2): i, j len(num1) - 1, len(num2) - 1 carry 0 res [] while i 0 or j 0 or carry: x int(num1[i]) if i 0 else 0 y int(num2[j]) if j 0 else 0 s x y carry res.append(str(s % 10)) carry s // 10 i - 1 j - 1 return .join(reversed(res))你会发现它跟plusOne的核心逻辑几乎一样从低位开始逐位相加记录进位最后处理残余进位。理解了加一这个模板你就能很自然地写出来。6.2 刷题心得我自己的体会是做这类简单题最重要的不是把答案背下来而是把“为什么这样做”讲清楚。哪怕面试只考这一题面试官也会追问边界条件和复杂度能讲明白才说明你真会了。这道题还有一个特别适合初学者的点它训练的是“从错误中学习”。我第一次写的时候也没注意到全9的扩容问题后来是被测试用例[9, 9]教做人的。从那之后我每次做数组题都会条件反射地问自己如果所有元素都是边界值会发生什么这个“边界值反射”的习惯比刷十道题都值钱。因为你刷题时常见的bug十有八九出在边界上而不是主流程上。最后再分享一个小技巧如果面试时碰到这道题可以先说出一个最直观但错误的方案比如“把数组转成数字直接加一”然后自己否定它再说出正确的模拟进位方案。这样能展示你对大数溢出问题的敏感度比直接闷头写代码要加分。还有个小细节用Python写的时候很多人会写digits [1] [0] * len(digits)这里的长度直接用原数组长度就行因为它表示全9情况下每一位都变成0加上开头多出的1。长度不用写成len(digits) 1因为原数组长度n全9加一结果是1后面n个0。最近再做这道题时我仍然会先默写一遍边界用例再写代码。如果你刚开始刷题建议也把这道题的几种写法都写一遍尤其是Java和Python各写一次手感会很不一样。毕竟力扣第66题“加一”作为经典入门题值得你花半小时彻底吃透。