LeetCode(力扣)上有一道叫“加一”的简单题,英文名是Plus One。我见过不少第一次刷它的朋友卡在同一个地方:题目明明给了一个 int 数组,他们下意识就想先把这个数组转成一个整数,然后加一,再转回数组,结果不是溢出就是各种边界错误。这篇文章想跟你好好聊聊这道题到底在考什么,为什么它经常被收进力扣的入门刷题清单、热题分类,以及怎样写出一份能直接通过全部测试用例的实现。
题面确实不长:给你一个非空整数数组 digits,用来表示一个非负整数。数组的每一位都存单个数字,比如 [1, 2, 3] 就代表整数 123,要求你对这个数加一,然后返回结果数组。如果输入是 [1, 2, 3],输出应该是 [1, 2, 4];如果输入是 [9, 9, 9],输出应该是 [1, 0, 0, 0]。
如果你刚开始刷算法题,这道题很适合拿来锻炼“从题干提取约束”的能力;如果你准备技术面试,它又是一道高频的快速手写题,考察你对数组边界和进位的敏感度。下面我从题目分析开始,一步步把这道题拆开讲透。
1. 先读懂题干:这道题实际在考什么
1.1 “数组表示数字”到底是什么意思
数组表示数字并不是让你把它当成数组字面量来读,而是当成十进制展开式。
[4, 3, 2, 1] 表示的是:
4 * 1000 + 3 * 100 + 2 * 10 + 1
也就是 4321。所以数组下标越靠后,对应的十进制位越低;下标 0 是最高位,下标 length - 1 是最低位。我们要做的是在最低位上加一,然后正确处理进位。
这个概念看起来简单,但很多人写着写着就忘了“高位在前、低位在后”的顺序,尤其是从后往前遍历的时候下意识会写错循环边界。我建议你先在草稿纸上把 [9, 8, 9] 这种输入展开一下:它表示 989,加一之后是 990,所以结果数组应该是 [9, 9, 0]。这个例子能帮你确认,加法的入口永远是最右边的个位,不是最左边的千位。
1.2 很多人的第一反应:转成整数加一
我先说一个最常见的错误方案:把数组里的数字拼成一个整数,比如 [1, 2, 3] 拼成 123,然后加一得到 124,再把 124 拆成 [1, 2, 4]。
这个思路在数学上没错,但在算法题里踩了一个大坑:数组的长度可以超过普通整数类型的上限。Java 的 int 最大能表示 2147483647,long 最大也就 9223372036854775807,也就是 19 位。JavaScript 的 Number 安全整数范围是 2^53 - 1,大约 16 位。而题目的数组长度完全没有给上限,极端情况下可能给你几十上百位甚至更长。一旦数组代表的数字超出类型范围,直接转整数的方案就会溢出,后面所有计算都失去意义。
还有朋友会说“我用 Python,int 不限长度”。Python 的 int 虽然不会溢出,但如果你先把数组拼成字符串再转 int,做完加法再拆回数组,这本质上是绕开了这道题真正想让你练习的“手工进位”过程。题目要你模拟十进制加法的逐位计算,而不是借用语言本身的大整数能力,面试官听到这种思路也很难给你加分。
1.3 非空数组、无前导零这两个条件不白给
题目里有两个看似普通的限制:digits 是非空数组,整数不包含任何前导零,除了数字 0 本身。
非空意味着你不用处理“输入为空数组”的情况,这是出题人主动帮你减少了一类边界分支。无前导零则意味着合法输入不会出现 [0, 5] 这种数组,因为 [0, 5] 和 [5] 都表示数字 5,会带来二义性。不过数组内容为 [0] 是合法的,它代表整数 0,这时候加一应该返回 [1]。
这两个条件不直接参与算法,但它们决定了你的代码不需要额外考虑空指针、空数组、前导零过滤这些场景。如果你在面试中能主动提起这一点,说明你审题够细致,会优先识别边界条件。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心解法:反向遍历,遇到非 9 直接加一返回
2.1 先模拟一遍“列竖式加法”
回想一下小学做加法时的步骤:从个位开始,两数相加,如果当前位结果超过 9,就把个位留下,向高位进一。这道题只是“加一”,所以进位只会是 1,不会出现其他值。
看几个输入:
- [1, 2, 3]:个位是 3,3 + 1 = 4,没有进位,结果 [1, 2, 4]。
- [1, 2, 9]:个位是 9,9 + 1 = 10,所以个位变成 0,向十位进一;十位是 2 + 1 = 3,没有继续进位,结果 [1, 3, 0]。
- [9, 9, 9]:个位 9 加一变成 0,进位;十位 9 加进位变成 0,再进位;百位 9 加进位变成 0,再进位,最后数组需要扩展一位,结果 [1, 0, 0, 0]。
从这个模拟里能提炼出一个关键规律:只有当某一位是 9 时,加一才会产生进位;如果某一位小于 9,那么加一之后直接结束,后面的高位根本不需要动。
2.2 反向遍历法的标准代码
这个规律对应到代码上非常简短。Java 实现可以这样写:
java复制class Solution {
public int[] plusOne(int[] digits) {
// 从最低位开始向前遍历
for (int i = digits.length - 1; i >= 0; i--) {
// 如果当前位不是 9,加一后不会进位,直接返回
if (digits[i] < 9) {
digits[i]++;
return digits;
}
// 如果当前位是 9,加一后变成 0,继续处理前一位的进位
digits[i] = 0;
}
// 所有位都是 9,循环结束后还没有返回,说明需要扩容
int[] res = new int[digits.length + 1];
res[0] = 1;
return res;
}
}
Python 实现同样简洁:
python复制class Solution:
def plusOne(self, digits: List[int]) -> List[int]:
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)
这段代码的关键分支就两个:
- 当前位小于 9,直接加一,返回原数组。
- 当前位等于 9,把它置成 0,继续往前检查。
一旦理解了“只有 9 才会进位”这个性质,你就不需要每次都用 sum 和 carry 去计算。因为传入的加数是 1,每一位的结果只有两种可能:要么加一后小于 10,要么加一后等于 10。小于 9 的位落在第一类,等于 9 的位落在第二类。
2.3 循环结束后为什么要新建数组
假设输入是 [9, 9, 9],执行循环:
i = 2,digits[2] = 9,置 0;
i = 1,digits[1] = 9,置 0;
i = 0,digits[0] = 9,置 0。
循环结束后 digits 变成 [0, 0, 0],但这不是答案。正确结果是 [1, 0, 0, 0]。原来的数组长度只够存放三位,而现在的结果需要四位,所以必须重新申请一个长度加一的数组。
Java 中 new int[] 默认会把所有元素初始化为 0,所以只需要把最高位 res[0] 设为 1,后面的 0 自动就有了。Python 里可以写成 [1] + [0] * len(digits),表示在开头放一个 1,后面补和原数组等长的 0。
这个“循环结束后仍未返回”的情形只对应一种输入:数组里每一位都是 9。一旦出现这种情况,加一后数字的位数会增加一位,结果是 1 后面跟着若干个 0。
3. 从“全 9 特判”到通用写法:两种思路的对比
3.1 先判断是否全是 9,代码会不会更简单
反向遍历法其实已经隐式处理了“全 9”的情况,但有些初学者反应不过来,于是会先想到单独判断:如果数组全是 9,直接返回 1 加 n 个 0;否则再反向遍历。
这种“先特判、再处理”的思路也完全正确,而且更容易讲清楚。Python 可以写成:
python复制class Solution:
def plusOne(self, digits: List[int]) -> List[int]:
# 先判断整体是不是全 9
if all(d == 9 for d in digits):
return [1] + [0] * len(digits)
# 再执行逐位加一
i = len(digits) - 1
while i >= 0:
if digits[i] == 9:
digits[i] = 0
i -= 1
else:
digits[i] += 1
break
return digits
这个实现把最特殊的场景单独拎出来,后面的循环就不用考虑“数组扩容”这件事,因为此时已经保证至少有一位不是 9,加一之后结果位数不会变长。
3.2 两种写法的时间差异:都是 O(n),但常数不一样
如果你只是看最坏时间复杂度,两种写法都是 O(n),因为循环最多走完整个数组。
但全 9 特判版通常需要扫描数组两遍:第一遍用 all 判断是不是全 9,第二遍从末尾往前进位。反向遍历法只需要一遍循环,遇到第一个非 9 就返回,常数更小,代码也更短。
我建议优先掌握反向遍历法。原因不只因为代码短,而是它把“判断是否全是 9”这个结论内化到了循环过程里。你不需要在循环前单独问“这数组是不是全 9”,你只负责从低位往高位走,碰到 9 就消掉进位,碰到非 9 就结束。这个模式在后续做字符串加法、链表加法时更通用。
当然,如果你在面试时紧张,先用“全 9 特判”把思路讲清楚,再补一句“这里可以先判断是否全 9,否则逐位进位”,面试官也能接受。至少证明你看到边界了。
3.3 我的个人排序:反向遍历 > carry 变量位运算
还有一种写法是维护一个 carry 变量:
java复制public int[] plusOne(int[] digits) {
int carry = 1;
for (int i = digits.length - 1; i >= 0; i--) {
int sum = digits[i] + carry;
digits[i] = sum % 10;
carry = sum / 10;
if (carry == 0) {
return digits;
}
}
int[] res = new int[digits.length + 1];
res[0] = 1;
return res;
}
这段代码也很优雅。它把“当前位是否进位”“当前位加完是多少”用数学方式表达,适合当你以后遇到“加一”变成“加 k”时复用。但就本题而言,因为 k 固定等于 1,carry 只会是 0 或 1,直接判断当前位等不等于 9 更直观,少了取模、除法这些运算,看代码的人一眼就能懂。
4. 边界用例和代码细节:提交前必须重点检查
4.1 建议自测的几组输入
我总结过一套专门针对“加一”的自测用例,你在自己 IDE 里先跑一遍,再提交力扣,能过滤掉绝大多数问题:
| 输入数组 | 期望输出 | 考察点 |
|---|---|---|
| [0] | [1] | 单独只有一位 0 的情况 |
| [5] | [6] | 普通一位数 |
| [9] | [1, 0] | 末位是 9,产生进位并扩容 |
| [1, 2, 3] | [1, 2, 4] | 普通多位数,无进位 |
| [1, 2, 9] | [1, 3, 0] | 只有个位触发进位 |
| [8, 9, 9] | [9, 0, 0] | 连续进位但首位非 9,不扩容 |
| [9, 9, 9] | [1, 0, 0, 0] | 全部是 9,需要数组扩容 |
4.2 原地修改和返回新数组怎么选
很多数组题会区分“原地修改”和“允许返回新数组”,但 LeetCode 的“加一”并没有特别强调这一点。官方题解通常直接返回修改后的数组。
当 digits[i] 小于 9 时,我们原地加一然后返回原数组,这样可以节省一次数组拷贝。当所有位都是 9 时,原数组长度不够,必须新建数组,无法原地完成扩容。
这里有一个容易被忽视的细节:如果你在前面没有修改原数组、而是用一个 List 来存每一位,那所有情况都会产生新对象,代码运行没有问题,但会比标准解法多做很多次元素拷贝。面试时可以提一句“最高位不是 9 的情况下,我直接复用原数组,只在极端情况下创建新数组”,说明你对空间使用心里有数。
4.3 新数组容量千万别写成 length + 2 或 length
我见过有人写出 new int[digits.length + 1] 后,又担心少了,结果用 length + 2,最后返回的数组多了个 0,比如 [9, 9] 变成 [1, 0, 0, 0] 而不是 [1, 0, 0]。加一后最多只会多出一位,因为 99...9 + 1 = 100...0,位数从 n 变成 n + 1,不会变成 n + 2。
还有一种写法是把循环结束后所有元素都手动置 0,再赋首尾。其实没必要,Java 的 int 数组默认初始值就是 0,Python 的 [0] * n 也会创建全都是 0 的列表。只设置最高位为 1 就可以。
5. 复杂度和同类题扩展:从“加一”到一套通用框架
5.1 时间复杂度与空间复杂度要分情况说
很多人一看到简单题,直接写一句“时间复杂度 O(n),空间复杂度 O(1)”就完了,但这个表述其实不够严谨。
时间上,最坏情况是数组全是 9,需要完整遍历一次并创建新数组,复杂度 O(n)。最好情况是最后一位小于 9,只访问一次就返回,复杂度 O(1)。
空间上,如果不需要扩容,比如 [1, 2, 3],我们直接在原数组上操作,额外空间是 O(1)。如果需要扩容,比如 [9, 9, 9],要新建一个长度为 n + 1 的数组,额外空间是 O(n)。
所以更准确的描述是:时间复杂度最坏 O(n)、最好 O(1),空间复杂度最坏 O(n)、最好 O(1)。把最坏和最好分开说,能让面试官看出你分析过边界。
5.2 同一套思路能解决哪些变体题
“从低位往高位处理,用一个变量存进位,结束后检查是否还有进位”这套框架,可以沿用到不少题上:
- 力扣 415 字符串相加:两个字符串表示的数字,从末尾开始逐位相加,维护 carry 变量。
- 力扣 2 两数相加:两个链表分别表示倒序数字,同样从链表头开始逐位加,处理 carry,结束后如果 carry 为 1 还要新增一个节点。
- 力扣 67 二进制求和:两个二进制字符串相加,只是进位条件从 sum / 10 变成 sum / 2。
这道“加一”就是这些题里最简化的模型,因为只有一个数字在加一,不需要考虑两个数字长度不同、不需要逐位取两个加数这种复杂情况。你把这个题目的循环结构吃透,之后遇到“K 进制大数加法”,只要把当前位的计算结果拆成 digit 和 carry 两部分,就很容易迁移。
5.3 用“列竖式”三个字向别人解释思路
如果让我用一句话跟别人讲这道题,我不会上来甩代码,而是说:你小学怎么列竖式算 989 + 1,代码就怎么写。
列竖式的过程是:从个位开始加,超过 9 就往下一位进 1,直到某一位加完不需要进位为止。换到数组上,就是从尾部开始遍历,遇到 9 就把当前位变 0,继续朝前;遇到不是 9 的位,加一之后立刻停止。这也正好解释了为什么最优解长成那样:因为它不是套模板,它就是在用程序描述一个你已经熟练了十几年的手算过程。
6. 实战复盘:我第一次刷这道题时的完整思考过程和踩坑记录
6.1 我第一次提交时错在哪里
我最初写这道题时,用的就是“数组转整数”的思路。网上搜索到的很多旧题解里也有类似做法,因为早期测试数据没现在这么强,数组长度比较短,转成 long 也能过。但现在力扣的测试用例会包含几十位的大数,直接转 long 会溢出,不可能通过。
因为之前用 JavaScript 比较多,我第一次写的时候还下意识想用 digits.unshift(1) 来模拟数组头部插入,用 [9, 9] 自测时确实能返回 [1, 0, 0]。可后来意识到,unshift 的时间复杂度是 O(n),它会移动数组里所有已有元素。在长度为 n 的数组上,如果所有位都是 9,unshift 本身带来的开销其实和最坏情况下的整体 O(n) 在一个量级,不算致命,但如果在面试里写,面试官可能追问“你知不知道 unshift 会造成 O(n) 的移动开销”,与其解释这些,不如直接用新建数组的方式,逻辑更清晰。
后来我换成了标准反向遍历法,连续几个边界用例都过了,才感觉到自己之前还是太依赖语言提供的便捷方法,没有回到“手写加法逻辑”这个本源。
6.2 如何用两分钟在面试中完整讲清解法
你在面试或自我讲解时,可以按这个顺序说明:
第一,明确不能用整型转换,因为数字长度可能超过类型上限,因此要模拟逐位加法。
第二,从数组最后一位开始遍历,如果当前位不是 9,直接加一返回;如果当前位是 9,将它变成 0,继续处理前一位。
第三,如果遍历结束都没有返回,说明原数每一位都是 9,加一后需要多出一位,因此创建一个长度加一的数组,最高位填 1。
这三句话基本就是完整题解。代码量不大,但每一步都有明确的判断依据。如果你能把“为什么不是 9 就能 return”讲清楚,面试官通常不会再追问细节。
6.3 我养成的刷题习惯:先写测试用例,再写实现
刷这道题让我养成一个习惯:拿到任何数组题,先别急着写核心逻辑,先在注释里列三到五组边界输入和期望输出,尤其是“最短输入”“最长进位情况”“全部相等数字”这类极端场景。
对“加一”来说,[0]、[9]、[9, 9, 9] 就是无论如何都必须跑通的三个例子。先跑完它们,再跑普通用例 [1, 2, 3] 和 [8, 9, 9],你会发现代码的每个分支都有对应测试覆盖,提交时心里踏实得多。
后来我遇到其他题,比如“字符串相加”“数组去重”“链表反转”,也会沿用这个习惯。这比单纯背诵代码样本更能训练算法思维,因为你真正把终点放在“理解边界”上,而不是“默写答案”。如果你也在按顺序刷力扣的热门 100 题或入门数组题,遇到“加一”时不妨多花几分钟做一次边界用例整理,这道题虽然简单,但值得当成一道“审题 + 边界 + 模拟进位”的综合练习题来对待。
