LeetCode 的“加一”,英文名 Plus One,中文平台叫力扣第 66 题。这道题是我在数组类题目里刷到的第一个“看起来过分简单,但每次都能聊出新东西”的题。尤其适合刚开始刷 LeetCode 的人、准备面试要快速过一轮经典题的人、以及日常需要处理大数加减法的同学。
表面需求一句话就能讲清楚:把一个 int 数组表示的整数加一,然后把结果继续用数组返回。比如输入 [1,2,3],输出 [1,2,4]。可一旦把 [9]、[9,9,9] 这类进位用例丢进去,就会发现很多上一秒还在“写普通数字加法”的人会立刻卡壳。这篇就来拆一下,为什么这题的标准解法是倒序遍历,以及有哪些坑容易踩。
我先放结论:这道题本质上是一个“十进制加一进位模拟”,它的关键不是怎么把数字相加,而是怎么处理“9 这个数字带来的连锁进位”,以及“全是 9 时数组要扩容”这个边界。
1. 题目拆解:它到底想让你做什么
1.1 还原题目,别让“数组”两个字骗了
在力扣上打开这题,你会看到类似下面的描述:给定一个由整数组成的非空数组,这个数组表示一个非负整数,数组首位是这个整数的最高位,每个元素只包含单个数字。请你把这个整数加一,并返回同样格式的数组。
为了把题意翻译成人话,你可以在脑子里做一个映射:
| 数组表示 | 实际整数 | 加一结果 | 返回数组 |
|---|---|---|---|
| [1,2,3] | 123 | 124 | [1,2,4] |
| [4,3,2,1] | 4321 | 4322 | [4,3,2,2] |
| [1,2,9] | 129 | 130 | [1,3,0] |
| [9,9,9] | 999 | 1000 | [1,0,0,0] |
如果你只是把数组转换成整数,再加一,再转回数组,那其实你根本没有用到数组这个数据结构的特性,而是在“绕开”它。LeetCode 把题目设计成数组形式,目的就是让你直接在这个数组上模拟十进制加法过程,而不是调用语言自带的大数转换。
这道题的所有难点都集中在两个地方:一是低位发生进位后,这种进位会不会一路传播到最高位;二是如果所有位都是 9,那加完以后数组长度会发生变化,必须重新分配空间。把这两个点想清楚,代码只是几行循环的事。
1.2 一条道走到黑的错误解法:转成数字再加一
我看到很多初学者拿到这道题的第一反应是:
java复制int num = 0;
for (int digit : digits) {
num = num * 10 + digit;
}
num = num + 1;
// 再把 num 转成字符串,然后拆成 int[]
这个思路非常直观,但放着不管的话,一定会出问题。Java 的 int 只有 32 位,大约能表示 21 亿以内的数,数组一长就溢出;用 long 也不行,long 最大也就 9223372036854775807,只能容纳 19 位数字。而题目没有限制数组长度,它的测试用例可以给你来一个几十位甚至上百位的数组,远超任何基础整数类型能表达的范围。
JavaScript 和 Python 的情况也不太一样。JavaScript 的数字类型是双精度浮点数,超过 Number.MAX_SAFE_INTEGER(也就是 2 的 53 次方减一)之后,整数精度就会丢失。比如 [9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9] 这个 17 位数字,转成 JS 的 number 后可能已经变成另一个数了,你再加一,得到的结果从一开始就是错的。
Python 虽然内置了大整数,理论上 int 不限制长度,你可以把数组拼接成一个大整数再算回来。但这样做的问题是:它暴露不出你对“进位”这件事的理解。面试官如果让你手写这道题,看到你 int("".join(map(str, digits))) + 1,大概率会追问一句:“如果只能用数组操作,你还能写吗?”一旦你只能靠大整数兜底,这道题的核心考点就被你绕过了。
1.3 决定思路的关键观察:进位只在 9 身上发生
抛开各种花哨写法,这道题真正值得观察的就一条:一个十进制整数加一,除非当前位是 9,否则根本不会影响更高位。
为什么?因为加一只会在末位增加 1。如果末位是 0 到 8 之间的任意数字,加完之后不会产生进位,前面的所有位都保持原样,整个操作当场结束。真正麻烦的只有 9,因为 9 加 1 会变成 10,当前位必须变成 0,然后向更高位进 1。
进上去的这个 1,又会对更高位做同样的事情。如果更高位也是 9,那它继续变 0、继续进位,这样一路往左传播;如果遇到某个不是 9 的位置,那这个位置加一,所有进位结束;如果一直走到最左边发现所有位都是 9,比如 [9,9,9],那每一位都要变成 0,并且还要在最前面多出一个 1,结果变成 [1,0,0,0]。
如果你还是觉得抽象,可以想象一个汽车里程表,当前显示 000999,再加 1 公里,右边三位会全部翻成 0,同时第四位从 0 翻到 1,变成 001000。这个从右往左翻动的过程,和这道题的处理顺序一模一样。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 标准解法与代码实现
2.1 用“倒序遍历加一”把过程写清楚
既然进位只能从低位往高位传播,那我们的遍历方向也必须是倒序,也就是从数组的最后一个元素开始,一路往前扫描。
整个算法可以浓缩成三步:
- 从最低位开始倒序遍历数组。
- 如果当前位不是 9,就把它加 1,然后立刻返回数组。
- 如果当前位是 9,就把它改成 0,然后继续看前一位;如果数组里所有位都是 9,全部改成 0 后,还要新建一个长度加一的数组,把首位设为 1。
为什么遇到非 9 位能直接返回?因为加一操作只加了 1,不会产生“还需要继续加”的情况。只要某一位成功完成了加一,后面的高位就完全不需要再动。比如 [1,2,8] 只需要把 8 改成 9,[1,2,3] 只需要把 3 改成 4。如果末位是 9,例如 [1,2,9],这个 9 要改成 0,并继续看前一位 2,2 不是 9,所以 2 改成 3 后直接返回,结果是 [1,3,0]。
你不需要提前复制数组,也不需要先用 List 再转回数组。题目的输入就是一个可变的 int[],在绝大多数语言里,直接改原数组是允许的,LeetCode 判题时也只关心返回值对不对。
2.2 参考代码:Java、Python、JavaScript 对照
Java 版本是最经典的写法,适合大多数面试场景:
java复制class Solution {
public int[] plusOne(int[] digits) {
for (int i = digits.length - 1; i >= 0; i--) {
if (digits[i] == 9) {
digits[i] = 0;
} else {
digits[i]++;
return digits;
}
}
int[] result = new int[digits.length + 1];
result[0] = 1;
return result;
}
}
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] = 0
else:
digits[i] += 1
return digits
return [1] + [0] * len(digits)
JavaScript 版本需要注意 unshift(1) 会改变数组长度,刚好对应“全 9 导致数组加长”的边界:
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;
}
}
digits.unshift(1);
return digits;
};
你可能会问:为什么全 9 情况返回新数组时,Java 里不用手动把后面的元素设成 0?因为 Java 的 int[] 在 new 出来之后,所有元素会默认初始化为 0。所以你只需要把下标 0 的位置设置成 1,后面那些位自然就是 0。比如原数组是 [9,9,9],新数组长度是 4,new 出来以后是 [0,0,0,0],把第一位改成 1,得到 [1,0,0,0]。Python 里 [1] + [0] * len(digits) 是同样的效果。JavaScript 里因为没有“定长并默认填充 0”的数组构造习惯,直接 unshift(1) 更符合语言的表达方式。
2.3 一个千万要想通的细节:为什么遇到非 9 就立即 return
很多人在写这题时,脑子知道“应该倒序”,但代码却容易写成这样:
java复制for (int i = digits.length - 1; i >= 0; i--) {
if (digits[i] == 9) {
digits[i] = 0;
} else {
digits[i]++;
// 这里忘了 return
}
}
如果这里不 return,循环会继续往前扫描,这会导致什么问题?假设输入是 [8,9],第一步把 9 改成 0,第二步遇到 8,加一后变成 9,最后返回值是 [9,0]。看起来好像也算对了?因为 89 加一确实是 90。但再想一步:遍历完 [8,9] 后,循环还会继续吗?不会,因为 i 变负数后循环结束,最终输出 [9,0],看起来和正确答案一样。
但如果是 [1,2,3],也就是没有 9 的情况,第一步把 3 改成 4,没有 return,第二步继续处理 2,把它也加一变成3,第三步把 1 加一成 2,最终输出 [2,3,4]。这显然就错了。123 加一应该是 124,而不是 234。
这种 bug 的本质是没有理解“只加一,不连锁”。只有在遇到 9 把当前位置变成 0 之后,才需要继续往前看,因为有一个进位要往前传。如果当前位不是 9,进位已经结束,后面高位绝对不需要变动,所以必须立刻返回。
2.4 复杂度分析:时间与空间都要能说清
先说时间。这个循环最坏情况下要从数组末尾一路处理到开头,比如输入全部是 9,或者前面有很长一串 9,所以时间复杂度是 O(n),n 是数组长度。最好情况是输入为 [0,1,2,3],末位不是 9,只跑一次循环就结束了,但我们在分析复杂度时通常说最坏情况 O(n)。
再说空间。没有出现全 9 时,我们是在原数组上原地修改,额外空间是 O(1)。一旦出现全 9,需要新建一个长度为 n+1 的数组,额外空间变为 O(n)。有些题解会说这道题的空间复杂度是 O(1),那是不太严谨的,因为它忽略了这个数组变长的边界。在面试中把这一点主动讲清楚,比单纯说“O(1)”更能体现你考虑问题的完整性。
提示:这里的 O(n) 是必须要说出来的。你可能会觉得数组有时候只处理一位,但作为算法分析,必须考虑最坏情况,例如输入是 [9,9,9,9,9,9]。
3. 边界用例、本地调试与易错点
3.1 一上来就要测的几组用例
我平时刷题时有个习惯,拿到题目先把正常人想不到的边界用例写在草稿纸上,再开始写代码。这题我建议至少准备下面这组测试:
| 输入数组 | 模拟过程 | 预期输出 | 测试目的 |
|---|---|---|---|
| [1,2,3] | 末位加一,不进位 | [1,2,4] | 常规情况 |
| [1,2,9] | 9 改 0,2 加一 | [1,3,0] | 单次进位 |
| [8,9,9] | 末位 9 改 0,次位 9 改 0,8 加一 | [9,0,0] | 多次进位 |
| [9,9,9] | 全 9,数组扩容 | [1,0,0,0] | 最核心边界 |
| [9] | 个位 9,加一后变成两位数 | [1,0] | 单个元素扩容 |
| [0] | 题目允许的整数 0 | [1] | 简单加一 |
别看上面数据规模小,它们能覆盖代码里的每条分支。尤其是 [9,9,9] 这一组,如果代码没有处理“循环结束后返回新数组”的情况,大概率会返回 [0,0,0],也就是把每一位改没之后,忘记了最前面的那个 1。
3.2 本地搭一个五分钟的测试脚手架
LeetCode 的网页编辑器虽然自带执行按钮,但多组用例一起跑的时候不够直接。我的习惯是把代码贴到本地 IDE,写一个极简的测试入口,一次性输出所有边界用例。
Java 可以这样写:
java复制import java.util.Arrays;
public class PlusOneTest {
public static void main(String[] args) {
Solution solution = new Solution();
int[][] cases = {
{1, 2, 3},
{1, 2, 9},
{8, 9, 9},
{9, 9, 9},
{9},
{0}
};
for (int[] c : cases) {
System.out.println(Arrays.toString(c) + " -> " + Arrays.toString(solution.plusOne(c)));
}
}
}
Python 可以这样写:
python复制from typing import List
class Solution:
def plusOne(self, digits: List[int]) -> List[int]:
# ...
if __name__ == "__main__":
solution = Solution()
cases = [
[1, 2, 3],
[1, 2, 9],
[8, 9, 9],
[9, 9, 9],
[9],
[0],
]
for c in cases:
print(c, "->", solution.plusOne(c))
这里有一个特别容易让新手懵的细节:Java 里如果直接 System.out.println(solution.plusOne(c)),你看到的会是一串 [I@1b6d3586 这样的内存地址,而不是数组内容。因为 int[] 没有重写默认的字符串表示,必须用 Arrays.toString() 转换一下。很多人在本地调试时看到结果和自己预期不一样,不是因为算法错,而是因为打印方式不对,排查了很久才发现是输出的问题。
3.3 新手最容易翻车的三个位置
第一个位置:循环方向写反。有人把数组首位当成低位,用正序遍历,于是输入 [1,2,9] 时直接把第一位加一,变成 [2,2,9],然后返回。129 加一应该是 130,而不是 229。记住,数组的第一个元素表示最高位,它放在最左侧,而我们要处理的最低位在数组的最右侧,所以必须从右往左遍历。
第二个位置:没有处理数组扩容。如果输入 [9,9,9],你按普通逻辑循环,当 i 走到 -1 时表示所有位都发生了进位,此时每一位都已经被改成 0。如果这时候直接返回 digits,结果会是 [0,0,0]。正确答案是 [1,0,0,0],必须新建一个长度加一的数组,或者用前文提到的不同语言方式在数组头部插入 1。
第三个位置:把 9 判定成需要继续扫描的最后一种情况后,忘了区分“当前位是 9”和“当前位不是 9”时是否要继续处理。更具体地说,如果输入是 [1,2,9],9 改成 0 后继续看 2,2 不是 9,2 加一变成 3,立刻 return;如果输入是 [1,9,9],9 改成 0,倒序看第二个 9,也改成 0,再往前看 1,1 加一变成 2,立刻 return。每一个“当前位加一”的动作只发生一次,不要对非 9 位做反复加一。
4. 从数组到字符串、链表的三个变体
4.1 核心模型:通用逐位加法器
“加一”不是只有数组这一种考法。把它抽象一步,你会发现它其实是一个“从低位到高位逐位相加、维护进位”的模型。最通用的形式是:给定两个表示大数的结构,从最右侧开始,每一位相加,加上上一次的进位,结果除以进制得到当前位的值,结果除以进制向下取整得到下一次进位。
这就是所有大数加法的公共骨架。LeetCode 第 66 题只加了 1,所以进位最高只能是 1,这也是为什么代码里只要遇到 9 就能确定需要置零继续扫描。如果加的不是 1,而是另一个大整数,那就不存在“遇到第一个非 9 直接返回”的优化,而是要把每一位都加完,直到两个数都处理完且进位为 0。
4.2 字符串变体怎么做
如果输入不是整型数组,而是一个字符串,比如 "999",要求加一后返回 "1000",怎么做?核心仍然是倒序遍历,但要注意字符和数字的转换。
我推荐直接掌握 LeetCode 415“字符串相加”的通用模板,它能处理两个任意长度十进制字符串相加,也天然能处理“加一”:
java复制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';
}
sb.append(sum % 10);
carry = sum / 10;
}
return sb.reverse().toString();
}
这段代码的逻辑和数组版本是一致的。carry 记录的是要不要向高位进 1。每次都先累加上一次的进位,再把结果的个位拼到字符串末尾,最后把整个字符串反转,就得到了从高位到低位的正确顺序。
如果你只想解决“字符串加一”,可以只写一个类似的循环,只是另一个加数固定为 1。本质上没有任何新知识点,只要你理解了数组版本,字符串版本几乎是一行行翻译过去。
4.3 链表变体怎么做
链表场景下,题目通常是“加一链表”。如果链表的头节点是最高位,尾节点是最低位,那么加一操作同样要从链表尾部开始。但链表是单向的,不能像数组那样按下标直接访问尾部,所以有两种常见解法。
第一种是用递归,递归到链尾先处理最后一个节点,回溯时处理进位,返回一个进位标志给上一层。第二种更直观,先把链表反转,让最低位变成头节点,然后从头节点开始从头到尾遍历,模仿数组倒序遍历,处理完加一之后再反转回来。如果反转后遍历到尾时仍然有进位,就再插入一个新节点。
举个例子:链表 9 -> 9 -> 9,反转后变成 9 -> 9 -> 9,从头部处理,第一个 9 改成 0,进位 1;第二个 9 改成 0,进位 1;第三个 9 改成 0,进位 1;遍历结束后还有进位,于是末尾追加节点 1,此时链表是 0 -> 0 -> 0 -> 1,再反转一次,得到 1 -> 0 -> 0 -> 0。虽然代码长度比数组版多不少,但底层逻辑完全一致,都是“逢 9 置 0,找非 9 加一”。
5. 面试现场怎么讲清楚这道题
5.1 30 秒说清思路
面试官如果让你做这题,不要埋头写代码,可以先把自己的思路组织成几句话。我建议这样说:
“这道题是模拟十进制加法的进位。由于数组里首位是最高位,所以我会从数组末尾开始倒序遍历。如果当前位不是 9,直接加一并返回,因为加一不会继续影响更高位;如果当前位是 9,就需要把当前位改成 0,并继续向前处理。最坏情况是所有位都是 9,比如 [9,9,9],循环结束后每一位都已经是 0,这时需要新建一个长度加一的数组,首位放 1,其余默认为 0,得到 [1,0,0,0]。”
这段话既点明了遍历方向,也点明了边界条件,面试官基本马上能判断出你理解到位。
5.2 被追问时的四类问题
面试官很喜欢在这个题后面追加几个小问题,用来判断你真的懂了还是在背题。常见的有下面四个。
第一个追问:“如果这个数组不能修改原数组,你会怎么做?” 答案很简单,不是原地改 digits,而是先复制一份,再在复制出来的数组上做同样处理。LeetCode 的原题通常不限制修改原数组,但工程实践中往往不允许破坏入参,所以这个点值得提一嘴。
第二个追问:“为什么要从后往前扫,而不是从前往后?” 因为十进制加法的进位只会从低位往高位传播,从后往前扫可以在第一次遇到非 9 位时立即结束。如果从前往后扫,你无法提前知道低位是否会产生一连串进位,处理起来非常别扭,通常还得先把数组反转一次,绕一大圈。
第三个追问:“如果数字非常大,比如 100 万位,你的解法可行吗?” 完全可行,因为我们只用数组的每个下标存一位,每一位都在 int 范围内,内存允许的情况下长度不是问题。如果你选择转成普通 int 或 long,那就会在第一步就溢出,这就是为什么这题必须用数组模拟。
第四个追问:“你能说说最坏和最好的场景分别是什么吗?” 最好场景是末位不是 9,加完直接结束;最坏场景是所有位都是 9,例如 [9,9,9,9],需要遍历完整个数组,并且最终要新建数组。新建数组这一步也是空间复杂度能到 O(n) 的原因。
5.3 刷完这题之后,下一步做什么
每次刷完一道算法题,我都会习惯性地找几道同模型的题一起打包练,这样记忆特别牢。“加一”这个题做完以后,最值得立刻做的是三个题:
LeetCode 67“二进制求和”能帮你把“十进制加一”推广到“二进制进位”。LeetCode 415“字符串相加”能帮你把“数组内进位”推广到“任意长度字符串”。LeetCode 989“数组形式的整数加法”则更像是本体的加强版,它不只是加一,而是加任意一个整数。看起来三个题都比加一复杂,但因为核心思想相同,你只要吃透了 66 题,再看它们会轻松不少。
我个人在实际操作中的体会是:像“加一”这种简单题,最容易犯的错误反而不是算法复杂度,而是边界处理不完整。面试前与其把大量时间花在困难题上,不如先把这类经典简单题的边界讲得滴水不漏。它能体现的思路清晰度,有时候比一个复杂的动态规划更能给人留下好印象。
