☰
LeetCode 66“加一”题解:从暴力转换到最优进位模板
2026/10/20 8:41:15 网站建设 项目流程

说起 LeetCode 第66题“加一”,估计不少人都觉得它是再简单不过的开胃菜。我第一次做这题时也是这么想的:把数组转成数字,加一,再转回数组,不就完事了吗?结果在 [9,9,9] 上直接翻车。后来我才意识到,这道题虽然名字叫“加一”,实际上考的是大数加法、进位传递和边界条件处理,几乎每次面试季都会出来刷存在感。这篇文章就从题目拆解、思路演进、代码实现到变体扩展,完整聊聊这道“简单题”里到底有哪些值得抠的细节,适合准备校招、社招刷题的新手,也适合想复习基础算法的老手。

1. 题目本身没那么简单

1.1 题面原意与三个隐藏考点

题面用一句话就能说清:给你一个非空数组,数组里的每个元素代表一个十进制数字的某一位,下标从0到n-1恰好对应这个数字从最高位到最低位。要求在这个数字的基础上加一,然后返回同样格式的数组。比如 [1,2,3] 代表整数123,加一后是124,所以返回 [1,2,4];[4,3,2,1] 代表4321,加一后返回 [4,3,2,2];[9] 代表9,加一后是10,但它不能返回 [1,0] 之外的形式吗?不不,返回类型还是数组,所以答案就是 [1,0]。

题目看起来只有一句话,可是里面至少藏了三个考点。

第一个考点是大数溢出。数组长度可以达到任意规模,题目并没有限制它只够放进 int 或 long。很多人在第一反应里就想把数组拼成数字,然后数字加一再拆回去,这在数据量小的时候能跑通,可一旦数组有几十位甚至上百位,int、long 都直接溢出,结果完全不可信。算法题不是考语言内置的大整数类,而是考数据结构本身的操作能力,所以这条路从一开始就走偏了。

第二个考点是进位的传递。加一这个动作在十进制里意味着当前位加1,如果当前位变成10就要写0进1,然后下一位继续加1;如果下一位也变成10,还得继续往前进位。这意味着进位可能一直传到最高位,比如 [9,9,9] 加一后会变成 [1,0,0,0],数组长度都要增加一位。这一点恰恰是最容易被忽略的。

第三个考点是返回值的处理方式。因为数组长度可能变化,你不能总是原地打转。当所有位都是9时,原来的数组没法装下新数字,必须返回一个长度加一的新数组。很多人在这一点上卡住,不是算不出结果,而是不知道该怎么优雅地构造结果。

1.2 为什么明明“简单”却总有人挂

刷过题的人可能都有感受:简单题的通过率不一定高,“加一”就是典型。我见过不少平时算法能力还不错的朋友,在这道题上用过各种奇怪做法。有人拿到题就写int num = Integer.parseInt(Arrays.toString(arr).replaceAll(...)),这种代码在数组只有一两位时没问题,但一旦到了 [9,9,9,9,9,9,9,9] 就直接“表演”溢出。还有人知道不能转换数字,写了个循环从末尾开始进位,逻辑基本对了,却忘了在最高位也出现进位时新建数组,导致输出 [0,0,0] 而不是 [1,0,0,0]。

这些错误背后其实都是同一个问题:没有把“加法”还原成最朴素的逐位相加过程。越简单的东西,越容易想当然。等你仔细看一遍竖式加法,就会发现这道题根本没有那么玄。

2. 从暴力解到正解的完整推演

2.1 第一版暴力方案:为什么必须放弃

先说说我最初踩过的坑。看到 [1,2,3],我第一反应是:

num = 0 for d in digits: num = num * 10 + d num += 1 return [int(c) for c in str(num)]

这个思路非常符合直觉,把数组“拼”成一个整数,加一,再“拆”回数组。用几组普通数据测试,比如 [1,2,3] 得到 [1,2,4],[4,3,2,1] 得到 [4,3,2,2],看起来都正常。可是当我拿 [9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9] 去测的时候,数字长度已经超过32位整数的上限,如果用 int 直接溢出;用 Python 这种支持大整数的语言倒是不会溢出,但那相当于把问题抛给了语言,而不是自己解决。

这就像你在考场上问“能不能直接用计算器”——如果能用,题目就没意义了。算法面试要的是你在数组结构上完成“竖式加法”的能力,而不是依赖语言特性。

而且就算语言支持任意精度整数,转来转去还涉及字符串处理,效率比直接操作数组低得多。面试官一眼就能看出你不太理解这题想考什么。暴力方案可以帮你在纸上理解题意,但永远不是最终答案。

2.2 竖式加法:正解的思维方式

小时候学加法时,都是从个位开始逐位相加,满十进一。“加一”这个题本质就是一次竖式加法,只不过被加数只有最低位的1,其他位都是0。你可以把进位理解成“往后传的一股劲”:当前位加上进位数后如果等于10,就写0,再给下一位递一个1;如果不等于10,就直接写结果,并且可以提前结束,因为后面的高位根本不会再受到影响。

为什么提前结束是安全的?因为只有当进位是1时,高位才可能被加一。如果某一位数字加上进位后小于9,它就不会产生新的进位,那更高位就不会有任何变化。举个例子 [1,2,9]:从末位开始,9+1=10,写0进1;前一位2+1=3,小于9,直接返回 [1,3,0]。这个过程中最高位1从头到尾没有参与运算,这正符合实际情况:129加一得到130,百位的1确实没动。

这个“能早退就早退”的特性,让代码既简单又高效。

2.3 三种典型实现:循环、递归与数组扩容

正解的写法有很多,核心都是逆序遍历数组。用 Python 写一个最直观的版本:

def plusOne(digits): n = len(digits) carry = 1 for i in range(n - 1, -1, -1): total = digits[i] + carry digits[i] = total % 10 carry = total // 10 if carry == 0: break if carry == 1: return [1] + digits # 会在开头插入1 return digits

这里carry初始化为1,相当于从个位开始加一。每一轮算出total,用% 10得到留在当前位的数字,用// 10得到进位。一旦carry变成0,说明后续高位不受影响,直接跳出循环。最后如果循环结束后carry仍然是1,说明所有位都发生了进位,比如 [9,9,9],这时就需要在数组最前面补一个1。

用 Java 写可以更简洁,先判断数字是否小于9,这样能避免多余的取模运算:

public int[] plusOne(int[] digits) { int n = digits.length; for (int i = n - 1; i >= 0; i--) { if (digits[i] < 9) { digits[i]++; return digits; // 当前位会产生进位,直接返回 } digits[i] = 0; // 当前位是9,加1后变0,继续向前进位 } int[] res = new int[n + 1]; res[0] = 1; // 全部是9,最高位补一个1 return res; }

这个写法的精妙之处在于:如果digits[i] < 9,那么这一位加一后不可能产生进位,直接把这一位加一返回即可;如果digits[i] == 9,加一后变成10,当前位写0,继续向前遍历。循环结束后若还没有返回,说明原数组每一位都是9,此时新数组长度加一,第一位写成1,后面自动补0。

还有递归写法,但实际意义不大。递归会占用调用栈,而且这个问题天然可以用迭代解决,没必要增加额外空间。我在给朋友讲题时会提一句存在递归解,但自己写代码时更偏向迭代版本。

3. 代码细节、边界测试与踩坑实录

3.1 边界条件清单

刷题最忌讳只测题目给的示例。我发现很多人写完 [1,2,3] 的用例通过后就觉得大功告成,结果在边界用例上原形毕露。以下是我自己整理的“加一”边界测试清单,基本覆盖了所有可能出现的情况。

场景输入示例预期输出说明
最普通的情况[1,2,3][1,2,4]末位不加进位,直接返回
末位是9[1,2,9][1,3,0]个位变0,向十位进1
多位连续9[1,9,9][2,0,0]进位穿越多个9,直到遇到非9
所有位都是9[9,9,9][1,0,0,0]数组长度增加一位
只有一位不是9[8,9,9][9,0,0]最高位本身是8,不用扩容
数组只有一位[9][1,0]最简单但最容易漏的新数组场景
最高位为0的非正常输入[0,1,2][0,1,3]题目不保证这种情况,但代码也要能处理

这里面最值得警惕的是[1,9,9]和[9,9,9]这一对。前者最高位不是9,所以长度不变,但你要确保循环不会越过最高位;后者最高位也是9,必须扩容。很多错误写法在处理[1,9,9]时因为循环逻辑不对,把第0位也错误地归零,最后得到[0,0,0]。

3.2 十个容易踩的坑

我在实际刷题和给别的开发者做 code review 时,总结出以下高频错误。几乎每个错误都有自己的“毒性”,值得一条条看。

  1. 用整数或长整数存储整个数组。这是最常见的错误,遇到超长数组直接溢出,或者答题平台专门构造大数测试用例,结果就是失败。
  2. 只处理最后一位进位。有些人写了digits[n-1]++就返回,完全没有考虑9的情况。
  3. 循环里手动重置进位。有人会写if (digits[i] + 1 == 10),这在只有一位进位时碰巧能过,但一旦前一位也是9,这种判断就失效了。
  4. 忘记 new 一个新数组。当输入全是9时,直接在原数组digits[0] = 1,后面还是原来的9,输出就变成了[1,9,9]。
  5. 新数组长度算成 n 而不是 n+1。比如int[] res = new int[n],后面设置res[0]=1直接越界。
  6. 循环结束后的进位判断顺序错了。有人在循环内先把digits[i]改为0,循环结束后才 append 1,但忘记用carry判断是否真的需要 append。如果输入是[1,0],循环结束后没有进位,却还是强行扩容,答案就错了。
  7. 把数组当成字符串处理。StringBuilder拼接后 parseInt,这类代码看着优雅,实际既低效又可能溢出,面试官通常不会给高分。
  8. 忘记返回类型是int[]。有人用ArrayList收集结果,最后返回类型不匹配。
  9. 在 for 循环里用了 break 而不是 return。比如写了个循环处理到digits[i] < 9时digits[i]++; break;,然后直接 return digits,逻辑上没错,但不够直观,容易在后续修改时踩坑。更关键的是,如果你 break 后在函数末尾又写了扩容逻辑,可能会误触发。
  10. 没有测试全是9的极端情况。这不算代码错误,而是测试思路问题,但导致的后果和错误代码一样严重。

其中第4、第6和第10最容易同时出现。我建议你在写完代码后,把上面表格里的用例都跑一遍,并且刻意用[9]和[9,9,9]这两个用例检查扩容逻辑。

3.3 复杂度分析

这道题的最优时间复杂度是 O(n),n 是数组长度,因为最坏情况是全部位都是9,需要遍历每一个元素。空间复杂度通常被认为是 O(1),因为对于绝大多数输入,我们是在原数组上修改后返回;但如果所有位都是9,需要创建一个长度为 n+1 的新数组,这时的空间开销是 O(n)。

面试中如果被问复杂度,更好的说法是:平均时间复杂度为 O(1),为什么?因为随机数据里末尾为9的概率较低,多数情况下循环一两次就结束了。最坏时间复杂度是 O(n),空间复杂度最坏 O(n)。这种“平均低、最坏线性”的说法,比一句简单的 O(n) 更能体现你对问题的理解。

还有一个细节值得说:Java 版本里利用digits[i] < 9返回的方式,实际上在很多用例下只需要循环一次,比如 [1,2,3] 直接在末位加1就返回了,这比每次都用取模运算要快。所以代码不只是正确,还有性能上的考量。

4. 变体、扩展与面试官的真实意图

4.1 从加一到加K:大数加法的通用模板

面试官不会满足于只问一道题,他可能会紧接着问:“那如果让你在这个数组上加一个任意整数 k,而不是加一呢?”这就是把“加一”扩展成“加K”的通用大数加法问题。

思路也很简单,把carry初始化为k,然后从低位到高位逐位相加:

def plusK(digits, k): carry = k n = len(digits) for i in range(n - 1, -1, -1): total = digits[i] + carry digits[i] = total % 10 carry = total // 10 if carry == 0: break while carry > 0: # 如果所有位都处理完但还有进位,需要在前方插入数位 digits.insert(0, carry % 10) carry //= 10 return digits

这个模板看起来不复杂,但有几个细节要留意。第一,carry不再是简单的0或1,可能是一个多位数,比如加200,首次计算时carry=200,低位的total可能超过10很多,所以必须用%10和//10来拆分。第二,当遍历完所有数组位后carry仍大于0,说明高位还需要额外数字,比如数组是 [1],要加999,结果会变成 [1,0,0,0],这时就要在数组头部不断插入进位。

理解了“加K”,你就能应付面试官对“加一”的各种魔改。面试官通常不会要求你真的写一遍加K,但你能主动说出“这个代码可以推广成加任意大数”,本身就是加分项。

4.2 链表、字符串版本怎么套模板

除了数组,“加一”还可以出现在链表或字符串场景里。某厂面试题就喜欢把数字存成链表,并且链表的头节点可能是最高位,也可能是最低位。如果是“最高位在头节点”,处理起来和数组一样,从尾部开始找加法位置,再把进位往前传。但单链表不能从后往前遍历,最常见的做法是先反转链表,再用同样的逐位加一逻辑,处理完后再反转回来。

如果是“最低位在头节点”,那就更直接了,从头节点开始遍历,用carry逐位加一,遇到carry == 0提前结束,遍历完还有进位就追加一个新节点。这个过程本质上和数组加一没有区别,只是数据结构变了。

字符串大数加法也是同源问题。两个字符串表示的数字相加,从末尾开始逐位算,用一个carry保存进位,最后反转结果。你只要把“加一”的核心模板练熟,这些变体都会变得很轻松。我试着给朋友讲这些变体时,经常用一句话总结:先定位最低位,再处理进位,最后处理残余的进位。

4.3 面试官到底想通过这题看什么

“加一”在面试中出现频率这么高,不是因为面试官想为难你,而是因为它能用最简单的描述考察好几个维度的能力。

它能考察边界意识:你是否会主动想到全是9的输入,而不是只盯着普通用例。它能考察代码简洁度:同样实现加法,有的人写了十行,有的人用三行就能清晰表达,这背后是对“提前返回”和“数组扩容”的理解。它还能考察沟通能力:你拿到题后会不会先和面试官确认输入是否非负、可不可以新建数组、数组长度有没有上限,这些都是真实开发中需要养成的习惯。

另外,这道题非常适合做“热身题”。面试开始时让候选人先写一道“加一”,既能快速评估基础,又不会因为太难让候选人紧张。所以刷题新手千万不要因为它是简单题就跳过,它可能是你面试中的第一个机会。

5. 实操心得:我把这道题做成了“模板”

5.1 我常用的稳定写法

我平时刷题时最常用的是 Java 版本那个提前返回的写法。它在逻辑上天然减少了不必要的操作,而且非常好解释:只要当前位小于9,加一之后就不会产生进位,可以直接返回;如果当前位等于9,那就把它变成0,让进位继续往高位走;最后如果整个数组都是9,那就在最前面插入1。

为了让你能直接“抄作业”,我再贴一遍这个模板,并加上注释:

public int[] plusOne(int[] digits) { int n = digits.length; // 从个位开始向前处理 for (int i = n - 1; i >= 0; i--) { if (digits[i] < 9) { // 当前位不需要进位,直接加1结束 digits[i]++; return digits; } // 当前位是9,加1变成0,继续处理更高位 digits[i] = 0; } // 能走到这里,说明所有位都是9 // 例如 [9,9] -> [1,0,0] int[] res = new int[n + 1]; res[0] = 1; return res; }

这个模板的好处是,它把“继续进位”和“停止进位”天然分开。你不需要额外维护carry变量,因为当digits[i] < 9时,进位一定已经停止;当digits[i] == 9时,进位必然继续。理解了这一点,这个版本可以很自然地讲给面试官听。

5.2 简单题也能练出真功夫

很多人刷题有一个误区:看不起简单题,总想挑战难题。但“加一”这种题恰恰是训练代码直觉的好素材。我通常会用几个方法来榨干它的价值。

第一个方法叫多语言实现。用 Python 写一遍,用 Java 写一遍,再用 C++ 或 JavaScript 写一遍,感受不同语言对数组扩容、内存分配的处理差异。比如在 C++ 里用vector<int>,要扩容可以直接insert,但要注意复杂度;而在 Java 里必须new一个新数组,这就需要理解对象引用。

第二个方法叫手写测试用例。写代码之前,先在纸上列出关键用例,包括 [9]、[8,9,9]、[9,9,9]、[1,0,0]。这四个用例覆盖了“不进位、进一位、跨多位、全体进位”四种情况。如果你能在动手前把测试用例想清楚,写出来的代码基本不会离题太远。

第三个方法叫变形重述。尝试把数组加一变成链表加一、字符串加一、甚至二进制数组加一。二进制加一其实更简单,因为遇到0就变1返回,遇到1就变0继续进位,但核心思路完全一致。多做几次变形,你会对这类型题目有更深的肌肉记忆。

5.3 最后分享一个小技巧

我个人写这类题时有个习惯:先在注释里写出“最坏情况是什么”,再写代码。比如这道题,我会在函数上方写一行注释:// 最坏情况是所有位都是9,需要返回 [1,0,0,...,0]。这么做的原因很实际——当你把最坏情况明确写出来,代码自动就会往那个方向收敛。写注释不是浪费时间,它是帮你盯住边界的锚点。

这道题我后来在一次模拟面试里又遇到过。候选人用了暴力转数字的思路,我提醒他“如果数组长度为100呢”,他马上意识到溢出问题;改成逐位进位后,他写得很顺,但最后忘记处理全是9的扩容,输出变成了 [0,0,0]。我问他为什么没测 [9,9,9],他愣了一下,然后不好意思地说:“光顾着看题目示例了。”这就是大多数人在简单题上失分的真实原因——不是不会,而是不重视边界。你现在看到这里,已经比很多人少踩一个坑了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询