开头先啰嗦一句:如果你的刷题列表里躺着这道题,你可能觉得它简单到不值得写笔记。但等你真正在面试白板前写过一遍,或者帮人 review 过代码,你就会发现,越是这种看起来人畜无害的题,越容易在细节上翻车。“加一”这题我前前后后遇到过三次翻车现场:一次是新手直接用 int(''.join(...)) 转数字加一再转回数组,结果被大数溢出按在地上摩擦;一次是没考虑 999 这种全是 9 的输入,返回了 [0, 0, 0];还有一次是陷入了“要不要原地修改”的纠结,把简单问题复杂化。所以这次把题目的完整笔记整理出来,从题目本质、解法演变、边界条件到面试考察逻辑都梳理一遍,希望能帮你把这道题彻底吃透。
1. 题目本身很简单,为什么值得专门写一篇笔记
很多刷题的人有个误区,觉得简单题就是送分题,看一眼答案就划走。但实际上面试里遇到的中低难度题,恰恰是区分“背过答案”和“真懂原理”的分水岭。加一这道题,LeetCode 上是 Easy 难度,通过率却一直在 45% 上下徘徊,说明有一半的人第一次提交是错的。这个数据本身就说明问题了。
1.1 题目描述与本质拆解
原题说的是:给定一个由整数组成的非空数组所表示的非负整数,在该数的基础上加一。最高位数字存放在数组的首位,数组中每个元素只存储单个数字。你可以假设除了整数 0 之外,这个整数不会以零开头。
举个例子:
- 输入
[1, 2, 3],代表数字 123,加一后得到 124,输出[1, 2, 4] - 输入
[4, 3, 2, 1],代表数字 4321,加一后得到 4322,输出[4, 3, 2, 2] - 输入
[9],代表数字 9,加一后得到 10,输出[1, 0]
表面上看,这就是个“小学数学加法”的数组模拟。但它的本质其实是一个数字进位与存储结构转换的问题:你把一个整数按十进制位拆开,存进一个数组,然后对这个“拆开后的数字”执行一次加一操作,最后还要以同样的结构返回结果。
这里面的核心矛盾在于:数组的每一位都是独立的,而加法偏偏是带进位的。进位一旦产生,就会向高位传播,传播到最高位时还会导致数组长度增加。这个“长度变化”是所有新手最容易忽略的地方。
1.2 题目常见的三个翻车点
我见过最多的错误有三个:
第一,试图用语言自带的数字类型直接转换。比如 Python 里写 str(int(''.join(map(str, digits))) + 1)。这个写法在数字比较小的时候没问题,但题目并没有限制数组长度,一旦数组有几十位甚至上百位,int() 转换出来的数字早超过语言支持的整数范围了。Python 的 int 虽然理论上不限长度,但很多其他语言(比如 Java 的 int、C++ 的 long long)会直接溢出,这种行为本身就是面试官最不想看到的——说明你根本没有考虑数据规模。
第二,没有处理“全是 9”的情况。比如输入 [9, 9, 9],加一后应该是 [1, 0, 0, 0],数组长度从 3 变成了 4。有人写循环的时候只改了末尾的 9,没处理最高位的进位,最后返回 [0, 0, 0] 或者 [0, 0, 0, 0],完全不对。
第三,纠结于“原地修改”。题目没有说不能新建数组,但很多人看到“数组”就默认要原地操作,结果为了原地修改写了一堆复杂的指针移动,反而把自己绕晕了。其实这个题在原地修改和新建数组之间,时间复杂度都是 O(n),空间复杂度最坏情况下也只是 O(1) 或 O(n) 的区别,完全没必要为了“看起来更优”而牺牲代码清晰度。
所以这道题的价值不在于“难”,而在于它在很小的体量里,把进位传播、边界处理、数组与数字的映射关系全都考了一遍。它不是那种看一眼就知道答案的题,而是需要你在心里走一遍加法运算的完整过程。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心思路:从小学数学出发理解进位机制
先说结论:这道题的最优解法,时间复杂度是 O(n),空间复杂度是 O(1)(不考虑返回数组占用的空间)。思路简单到让人怀疑人生——从数组最后一位开始,模拟一次“加一”的竖式加法过程,唯一需要处理的就是“逢十进一”的传播。
但“简单”不等于“理所当然”。我在复盘的时候发现,真正把这道题吃透的人,都知道三条隐藏的观察结论,这三条结论才是写出优雅代码的基础。
2.1 观察一:加一操作只会影响从末尾开始的一段连续 9
这个观察是整个题目的题眼。你想象一下一个数在十进制下长什么样,比如 12345,加一之后变成 12346,只有最后一位变了。如果末尾是 1239,加一后变成 1240,影响到了倒数第二位。如果末尾是 1299,加一后变成 1300。如果是 9999,加一后变成 10000,每一位都变了,而且数组长度还多了一位。
也就是说,进位是从低位向高位逐位传播的,而且一旦某一位加一后不等于 10,进位链就断了,更高位完全不需要动。这就是“从后往前遍历,遇到不是 9 就停”这个策略的依据。
有些题解会把这个问题说成“找到最右边那个不是 9 的位置”,我觉得这个说法很精准。因为加一操作的本质就是:从末尾开始,把所有连续的 9 变成 0,然后把第一个不是 9 的那一位加一。如果整个数组全是 9,那就把所有位都变成 0,再在数组最前面补一个 1。
2.2 观察二:不需要“计算整个数字是多少”
很多人刚看到这道题的第一反应是:把这个数组拼成一个整数,然后加一,再拆回数组。这本质上是用计算器思维代替了算法思维。在真实面试中,这种解法暴露的问题有两个:一是没有考虑数据范围的溢出,二是没有抓住“数组本身已经给你分好了位”这个条件。
数组 [1, 2, 3] 和数字 123 之间不是“需要转换”的关系,而是“同一个东西的两种表示形式”。你手里已经拿到了每一位的值,直接用位运算的思路去做就行,根本不需要先转换成整数再转换回来。这就是数据结构题里常说的:题目给的存储结构本身就在暗示你该用什么方式处理。
2.3 观察三:结果数组长度的变化只可能发生一次
很多人被“数组长度可能加一”这个点吓到了,觉得要特殊处理。但仔细想想,长度变化只可能发生在所有位都是 9 的情况下。比如 [9, 9] -> [1, 0, 0],除此之外的任何情况,数组长度都不会变。
所以处理方式也非常简单:遍历一遍,如果全部是 9,就 return [1] + [0] * n;否则正常处理。很多题解里用 Python 写 [1] + [0] * len(digits),就是利用了这个观察。C++ 和 Java 则可以用 vector 或 ArrayList 在头部插入一个 1,从右往左插入也行,看个人习惯。
这三条观察放在一起,代码就呼之欲出了。不需要什么花哨的数据结构,不需要动态规划,不需要二分查找,一个循环解决问题。
3. 三种常见解法拆解:从直观到优雅
我刷这道题的时候,一开始也是从最直观的方式入手的,后来才逐步优化。这里把三种典型的解法都列出来,顺便分析一下各自的适用场景和隐藏的坑。注意,我给出的代码只是参考,实际面试中你可以用任何语言,关键是逻辑清晰。
3.1 解法一:数组转数字再转回(新手常见,不推荐)
python复制def plus_one_naive(digits):
num = 0
for d in digits:
num = num * 10 + d
num += 1
return [int(c) for c in str(num)]
先说这个解法为什么是错的:它把数组 [9, 9, 9, ...] 拼成了一个巨大的整数,然后加一,再转回字符串拆成列表。在 Python 里,由于 int 没有上限,这个解法在小数据下能跑通,但在其他强类型语言里,或者当数组长度超过 18 位时,就会出现整数溢出。即使 Python 能跑,这个解法也没有任何算法思想,纯粹是投机取巧。一旦面试官追加问一句“如果数组有 1000 位怎么办”,这个解法就彻底报废了。
3.2 解法二:逆序遍历 + 进位标记(最推荐)
python复制def plus_one(digits):
n = len(digits)
for i in range(n - 1, -1, -1):
if digits[i] == 9:
digits[i] = 0
else:
digits[i] += 1
return digits
return [1] + [0] * n
这就是标准答案了。逻辑非常清晰:从最后一位往前扫,遇到 9 就置 0 并继续往前看,遇到第一个非 9 就加一然后直接返回。如果循环结束了还没返回,说明所有位都是 9,这时候直接在数组最前面补一个 1,后面全是 0。
这个方法的时间复杂度是 O(n),最坏情况(全是 9)需要遍历整个数组;最好情况(最后一位不是 9)只需要 O(1)。空间复杂度 O(1)(原地修改),返回值另算。这里有一个小技巧:return [1] + [0] * n 比 digits.insert(0, 1) 更高效,因为 insert(0, 1) 在 Python 里是 O(n) 的,而列表拼接底层是直接创建新列表,虽然在“全是 9”的情况下两者都是 O(n),但拼接连表创建一次就行,逻辑上更清晰。
3.3 解法三:从前往后的找第一个非 9(换个角度,思路一样)
python复制def plus_one(digits):
for i in range(len(digits) - 1, -1, -1):
if digits[i] != 9:
digits[i] += 1
for j in range(i + 1, len(digits)):
digits[j] = 0
return digits
return [1] + [0] * len(digits)
这个解法的思路是:先找到最右边那个不是 9 的位置 i,把它加一,然后把它后面的所有位都置 0。比如 [1, 2, 9, 9],找到的 i 是下标 1(数字 2),加一变成 3,后面两位变成 0,结果是 [1, 3, 0, 0]。
这个写法在逻辑上更贴近前面说的“观察一”,代码也不算复杂。对比解法二和解法三,两者都是 O(n),但解法二更短更直接,解法三的循环嵌套看起来稍微绕一点。个人建议用解法二,面试时更容易解释清楚。
3.4 不同语言的实现细节差异
这道题在不同语言里的写法差异还挺有意思的,值得单独说一嘴。
C++ 版本:
cpp复制vector<int> plusOne(vector<int>& digits) {
for (int i = digits.size() - 1; i >= 0; --i) {
if (digits[i] == 9) {
digits[i] = 0;
} else {
++digits[i];
return digits;
}
}
digits.insert(digits.begin(), 1);
return digits;
}
C++ 里 insert(begin(), 1) 同样是 O(n) 的操作,但它会移动所有元素,所以如果追求效率,更推荐先 push_back(1) 再 reverse,或者直接新建一个长度为 n+1 的 vector。不过说实话,这种量级的优化在面试里完全没必要,反而显得你在吹毛求疵。
JavaScript 版本:
javascript复制var plusOne = function(digits) {
for (let i = digits.length - 1; i >= 0; i--) {
if (digits[i] === 9) {
digits[i] = 0;
} else {
digits[i]++;
return digits;
}
}
return [1, ...digits];
};
JavaScript 的 [1, ...digits] 展开运算符在数组长度为 n 时是 O(n) 的,但它在所有元素都是 9 的情况下才会执行,所以代价不高。如果想原地修改,也可以用 digits.unshift(1),同样 O(n),但语义上不如 [1, ...digits] 清晰。
Java 版本大同小异,用 ArrayList 的话可以 list.add(0, 1),或者先 list.add(1) 再反转,看你喜欢哪种风格。
这里强调一下:不同语言的时间复杂度不会因为 API 不同而改变,只是代码风格不同。重点是让面试官看到你理解了进位传播的本质,而不是炫技。
4. 边界条件不是细节,是这道题的隐藏考点
前面说过,加一这道题通过率只有 45% 左右,一大半原因是边界条件没处理干净。这一节把边界条件单独拎出来说,因为这是你在面试里能不能拿满分的分水岭。
4.1 常规边界:空数组、0、末尾是 9
先说空数组。题目明确说“非空数组”,所以这个边界不会出现,但如果你在实现时发现 digits 可能为空,可以加一个判断:if not digits: return [1]。不过题目没有这个用例,我一般不会主动加,因为面试官更希望你把精力放在核心逻辑上。
输入 [0],代表数字 0,加一后是 1,返回 [1]。这个用例很简单,但注意别把 [0] 当成空数组处理。
输入 [9],加一后是 10,返回 [1, 0]。这个用例就是典型的需要扩展数组长度的场景。很多人在这里会写错,因为他们只改了最后一位,忘了往前面补 1。
输入 [1, 9, 9],加一后是 200,返回 [2, 0, 0]。注意这里的进位传播到了第二位,但数组长度不变。如果你按解法二写,循环会先把两个 9 置 0,然后把 1 加一变成 2,最后返回 [2, 0, 0],完全正确。
4.2 极端边界:全部为 9 的长数组
比如 [9, 9, 9, 9, 9, 9, 9, 9, 9](9 个 9),加一后应该得到 10 亿,即 [1, 0, 0, 0, 0, 0, 0, 0, 0, 0](9 个 0)。在这种输入下,你的代码必须能优雅地处理长度变化。
解法二的做法是:循环把所有的 9 变成 0,然后循环结束,执行 return [1] + [0] * n。这里的 n 是原数组长度,所以结果数组长度是 n + 1,第一位是 1,后面全是 0。这是最简单的处理方式。如果面试官问你“为什么不用 insert”,你可以说:insert 在 Python 里是 O(n) 操作,而且要移动整个数组,直接用 [1] + [0] * n 创建新列表更清晰,也不会有多余的元素移动。
4.3 极端边界:超大数溢出测试
这道题的测试用例里通常会有长度为几十位的数组,用来测试你是否使用了整数转换。比如 [9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9](20 个 9)。如果你用解法一(拼数字),在 C++ 或 Java 里直接溢出;在 Python 里虽然能出结果,但你的解法本质上是错的,只是能跑而已。真正的“正确”是指你的算法在任意长度下都能以 O(n) 时间复杂度工作,而不是依赖语言的整数上限。
我建议你写代码的时候自己测一下这种超长用例,特别是用 Python 的解法一跑一遍,你会发现即使结果正确,面试官一追问也会露馅。所以在准备面试的时候,一定要把“无论多长都能处理”作为设计目标,而不是“当前语言能跑就行”。
4.4 边界用例清单
为了方便复习,我整理了一个测试用例表,建议你刷题的时候把这几个用例都跑一遍:
| 输入 | 预期输出 | 考察点 |
|---|---|---|
[1, 2, 3] |
[1, 2, 4] |
普通情况,末尾加一 |
[4, 3, 2, 1] |
[4, 3, 2, 2] |
普通情况,末尾加一 |
[0] |
[1] |
最小非负整数 |
[9] |
[1, 0] |
单个 9,数组长度变化 |
[1, 9, 9] |
[2, 0, 0] |
进位传播到非 9 位,长度不变 |
[9, 9] |
[1, 0, 0] |
全部为 9,长度加一 |
[9, 9, 9, 9] |
[1, 0, 0, 0, 0] |
全部为 9,长数组扩展 |
[8, 9, 9, 9] |
[9, 0, 0, 0] |
最高位前面的进位,长度不变 |
[9, 0, 9] |
[9, 1, 0] |
中间夹 0,只处理末尾连续 9 |
[0, 0, 1] |
[0, 0, 2] |
前导零情况(题目不保证,但可以留个心眼) |
这些用例覆盖了:普通情况、进位传播、长度扩展、中间有 0 但不影响进位传播等场景。如果你把这组用例全跑过了,这道题的边界基本就稳了。
5. 从一道题看一类“简单题”的面试考察逻辑
单纯把题目做出来,其实只完成了这道题 50% 的价值。剩下 50% 在于你能否通过这道题向面试官展示你的算法思维。很多候选人挂在简单题上,不是因为不会做,而是因为“只做了题没思考”。
5.1 面试官到底在考察什么
加一这道题在面试里出现的频率很高,因为它能在五分钟内考察出四个方面的能力:
第一,数据规模意识。你有没有在动手前问一句“数组最长有多少位”?这是区分“会写代码”和“有工程思维”的分水岭。如果数组长度超过 int 范围,用整数转换就是致命伤。
第二,进位与状态传播的理解。加一的本质是“从低位向高位的进位传播”,这个思想在二进制加法、链表相加、大数运算等很多题目里都适用。如果你能在面试时说一句“这题的本质和 LeetCode 445 两数相加 II 是一样的”,面试官会恍然大悟,觉得你是真的理解,而不是背题。
第三,边界条件的完备性。全 9 的情况一定要特殊考虑。很多人写代码的时候只想着“正常情况”,忘了极端输入,这就是通过率只有 45% 的原因。你在面试时主动把这些用例列出来,说“我需要考虑一下全是 9 的情况”,这个行为本身就是加分项。
第四,能否把简单解法讲清楚。用 O(n) 的解法时,你能不能解释清楚为什么从后往前遍历?为什么遇到非 9 就可以返回?这个“为什么”比代码本身重要得多。
5.2 从加一延伸开:同类题目的知识网络
加一其实是一个更大的知识点“逐位模拟操作”的入口。你在刷完这道题以后,应该主动把它跟下面几类题联系起来,形成知识网络,而不是一道题做完就完事。
第一类是链表相加。LeetCode 2 和 445 是“两个链表表示的数相加”,思路同样是逐位加法 + 进位传播。区别在于链表不能随机访问,所以需要先反转(或使用栈)才能从低位开始加。一旦你明白了加一里的“从后往前处理进位”,链表相加的“为什么先反转”就非常好理解——因为加法本身就必须从低位开始。
第二类是数组形式的加法。LeetCode 989 “数组形式的整数加法”是把一个数字的每一位也拆成数组,然后跟一个普通整数相加。写法比加一复杂一点,但核心逻辑完全一样:从个位开始逐位相加,处理进位。
第三类是二进制求和。LeetCode 67 “二进制求和”把进位从“逢十进一”变成了“逢二进一”,但代码框架是一模一样的,只是把 9 换成 1,把 10 换成 2。你如果能把加一的逻辑吃透,改几个数字就能写出二进制求和。
第四类是大数运算的通用思想。真实开发中遇到超过语言整数范围的大数计算时,基本思路就是“用一个数组/字符串模拟竖式运算”。加一就是最简单的竖式运算,后面还有大数乘法、大数阶乘,核心还是“进位”和“借位”。
5.3 面试现场的回答节奏
如果你在面试中遇到这道题,我建议你的回答节奏是这样的:
- 先问清楚数组长度上限,确认能不能用整数转换。这既是确认需求,也是展示你考虑数据规模的习惯。
- 简单说思路:从末尾遍历,遇到 9 置 0,否则加一返回。如果全是 9,就新建一个长度为 n+1 的数组,首位是 1。
- 说完思路再写代码,写完把
[9]、[9, 9]、[1, 9, 9]这几个用例在脑子里过一遍。 - 最后主动分析复杂度:时间复杂度 O(n),最坏情况遍历整个数组;空间复杂度 O(1) 原地修改,返回数组另算。
如果你能在 2 和 4 之间自然地说一句“这个解法的本质是进位传播,和链表相加是同一个模型”,这题基本就是满分操作。
6. 做题复盘:我在这道题上收获的三件事
这道题既不是最难的,也不是最经典的,但它对我来说是一个很好的“复盘样本”。刷完它以后,我强迫自己写了复盘笔记,提炼出了三条以后刷题也能用得上的经验。
6.1 先观察数字规律,再动手写代码
我刚开始刷题的时候,习惯性地拿到题目就写循环,写完再调试,浪费了大量时间。这道题让我意识到:很多“简单题”里藏着非常强的数字规律。加一的规律就是“末尾连续 9 的个数决定进位传播距离”,只要你发现了这个规律,代码就是水到渠成的事。
以后遇到任何跟数字有关的题,我都会先花两分钟在纸上算几个例子,看看有没有规律可循。这个习惯帮我在很多题上省了时间,比如回文数、整数反转、字符串转换整数等,其实都是先观察规律再写代码的典型题目。
6.2 边界条件要主动设计测试用例,而不是等测试告诉你
以前我写完代码,习惯直接提交,靠评测系统告诉我哪里错了。后来我发现这样效率太低,因为简单题的 WA(Wrong Answer)通常就集中在几个边界条件上,与其反复提交,不如在写代码之前就把边界用例在脑子里过一遍。
加一这道题让我养成了一个习惯:写完代码后,先自己构造边界用例跑一遍。对我的所有刷题流程来说,这个习惯能帮我减少至少一半的 WA 次数。特别是“空数组”“单个元素”“全部相同元素”这类极端用例,跑一遍基本能筛掉大部分低级错误。
6.3 做过的题要建立索引,方便面试前快速复习
这道题我在三个月后重新做了一遍,惊讶地发现自己竟然忘了怎么处理全 9 的情况。这让我意识到:刷题不能丢,还得有积累和复盘体系。所以我现在每做完一道题都会写一个简单的笔记,标题就是“题目名 - 题目笔记”,内容包括:核心解法、复杂度、边界测试用例、相关联的题目。等到面试前,我只需要翻这个索引,就能快速回忆起每道题的关键点,而不必把几百道题全部重新刷一遍。
加一这道题的索引笔记大概长这样:
text复制题目:加一(Plus One)
难度:Easy
核心思路:从末位往前遍历,逢 9 置 0,非 9 加一返回;全 9 则最前面补 1
复杂度:时间 O(n),空间 O(1)
边界用例:[9], [9,9], [1,9,9], [8,9,9,9]
关联题目:两数相加 II、数组形式的整数加法、二进制求和
备注:本质是进位传播,面试时主动说清“从后往前处理”的原因
7. 最后再分享两个代码之外的小技巧
代码本身大家都能写出来,我在实际复盘和帮别人 review 代码的时候,发现有两点是代码之外的、但对这道题非常关键的细节。第一个是关于“原地修改”的争议。很多人担心修改输入的 digits 会影响外部数据,所以在循环里复制了一份数组再操作。其实这道题在 LeetCode 上修改输入是没有问题的,因为函数返回后外部不会再用到原数组。但是在真实工程中,如果你不确定调用方是否还会继续使用这个数组,最好在开头用 digits.copy() 或 list(digits) 备份一份,或者在注释里明确写一句“本函数会修改原数组”。这个习惯在任何“数组作为输入”的算法题里都适用。
第二个是关于极端性能的测试。如果你把 [9] * 1000000 作为输入,解法二的性能表现会是:最坏情况下遍历 100 万次,然后创建长度为 100 万零 1 的新数组,总体 O(n),非常稳定。但如果你用解法一,Python 的 int 同样能转成 100 万位的整数,但 str() 和列表推导式的开销就会大很多,实际运行时间可能是解法二的几十倍。我用 timeit 跑过,1 万位的输入下解法二大概在微秒级,解法一在毫秒级,差距非常明显。这也再次说明了:不要用语言特性来掩盖算法缺陷。
最后给你一个非常具体的学习路径建议:做完加一这道题,花 20 分钟把“二进制求和”“数组形式的整数加法”“两数相加 II”这三道题看一遍,你会发现它们的核心循环体长得几乎一模一样。当你以后再看任何“逐位操作”的题,脑子里就不是一道题了,而是一张知识网络。这张网络,才是刷题真正积累下来的东西。
