☰
力扣66题“加一”详解:从数组模拟到进位逻辑的算法基本功
2026/10/15 4:57:33 网站建设 项目流程

最近在刷题时碰到一道很有“面试相”的题,力扣66题“加一”。题目描述特别短:给定一个由整数组成的非空数组表示的非负整数,在这个数的基础上加一,返回加一后的结果数组。但就是这道看似人畜无害的基础题,我见过不少人在面试时翻车,也见过讨论区里各种“把数组转成数字再加回来”的花式解法。刷完之后我才意识到,这道题的考点远不止“加一”本身,它几乎把数组操作、进位逻辑、边界处理、原地修改这几个基本功全揉在了一起。

这篇文章我会从题目语义、解题思路、代码实现、边界测试到复杂度分析,把66题从头到尾拆干净,再把我实际踩过的坑和面试中常见的追问整理出来。不管你是刚开始刷题的算法新人,还是准备面试、想查漏补缺的开发者,这篇内容都能给你一点参考价值。

1. 题目拆解:先搞清楚“加一”到底在考什么

1.1 题目的真实语义:数组是在模拟一个十进制数

先搞清楚输入是什么:[1,2,3] 这个数组,它表示的不是三个数字,而是整数 123。数组的每个元素只存 0 到 9 中间的一位数字,并且“最高位”放在数组的开头,也就是下标 0 的位置。题目要求的是把这个整数加 1,再以同样的数组形式返回。比如 [1,2,3] 加一就是 124,返回 [1,2,4];[4,3,2,1] 加一就是 4322,返回 [4,3,2,2]。

为什么要用数组存数字而不是直接给一个整数?这是理解题目的第一把钥匙。数组可以表示任意长度的整数,不受 int、long 这些数据类型长度的限制。只要能装进数组,位数再多也能表示。这道题在出题时故意去掉了很多干扰信息,只留下“数组模拟数字”这个核心抽象,目的就是逼你直接在数组上做运算,而不是借助语言自带的数字类型。

1.2 三个决定解法的关键信息

题目里还有几个容易被忽略的约束,我一开始就漏看了一个:“假设除了整数 0 之外,这个整数不会以零开头”。这句话的意思是说,输入数组不会出现 [0,1,2] 这种前导零的情况,唯一可能以 0 开头的就是 [0] 本身。这省去了很多麻烦,你不需要在结果里考虑“去掉前导零”的问题。

另一个关键信息是“非空数组”,也就是说输入至少有一个元素,不需要写空数组的特殊分支。最后一个隐含信息是:数组的每个位置只存储单个数字,也就是 0 到 9。这些看起来像是废话的约束,其实都是出题人帮你划重点:你只需要关心“加一”和“进位”,其他的边界已经帮你砍掉了。

1.3 这道题在算法体系中的定位

力扣66题通常被归为“数组基础”或“模拟题”。它不涉及复杂的数据结构,也不需要你掌握什么高级算法,但它考察的是最基础的“从低位到高位逐位处理”的思维。这种思维在后面做二进制求和、大数相加、字符串相加时都会反复用到。很多人觉得这道题太简单不值一提,但恰恰是这种简单的题,最能看出一个人写代码是不是干净、边界处理是不是严谨。

2. 先说说为什么“转数字”是条死路

2.1 很多人的第一反应:拼接字符串再转数字

说实话,我第一次刷这道题,脑子里冒出的第一个方案就是把数组拼成字符串,再把字符串转成整数,加一之后拆回数组。比如:

num = int("".join(map(str, digits))) num += 1 result = [int(x) for x in str(num)]

这段代码在 [1,2,3] 这种小数组上跑得欢快,看起来没有任何问题。但一旦数组稍微长一点,问题就出来了。在 Java、C++ 这类强类型语言里,int 最大只能表示到 2的31次方减1,也就是大概 21 亿。数组长度超过 10 位,就已经逼近 int 的上限了;超过 19 位,连 long 都会溢出。

2.2 大数溢出的原理分析

为什么会溢出?因为数组表示的数字可以无限长,而语言内置的数字类型长度是有限的。比如数组长度是 20,每一位都是 9,那这个数字是 99 亿亿级别,远超过 long 的 9223372036854775807。转数字的做法本质上是用“语言内置数字类型”去模拟一个“任意长度数字”,这在输入规模变大时必然失败。

有些读者可能会说:“我用 Python,int 是任意精度的,不就不会溢出了吗?”确实,Python 的 int 不限制长度,但面试官如果要考察你,根本不会让你停留在“能跑”的层面。他会追问:如果这个数组有 1000 位呢?就算 Python 能处理,转字符串再拆回数组的过程也引入了不必要的开销。更重要的是一旦你选择了“转数字”,你就放弃了“在数组上原地模拟加法”这个核心考点,面试成绩会大打折扣。

2.3 就算不溢出,转数字也不优雅

退一步讲,就算你用的是 Python,不溢出,转数字的方案仍然有两个问题。第一,代码的可读性依赖于语言特性,换一个语言就得重写;第二,这个问题本来考察的是数组和进位的逻辑,不是字符串处理。你绕了一大圈,反而失去了题目本来的意义。所以我强烈建议,遇到“数组表示数字”的题目,第一反应不要是“转成数字”,而是“逐位模拟”。

3. 正解:从低位到高位,用“进位”思维逐位处理

3.1 核心算法流程

正确的思路其实非常简单,就是模拟手算加法。我们从小做加法竖式,就是从个位开始,逐位相加,满十进一。这道题的输入是数组,低位在右边,所以要从数组尾部往前遍历。核心步骤只有四步:

  1. 从数组的最后一个元素开始,倒序遍历。
  2. 如果当前位置的数字小于 9,直接把这个位置的数字加 1,然后立刻返回整个数组。为什么可以立刻返回?因为这一位加 1 后不会超过 9,不会产生进位,前面的高位完全不受影响。
  3. 如果当前位置的数字等于 9,说明加一之后会变成 10,这一位要写成 0,并且把进位继续往前传递,所以接着看前一位。
  4. 如果循环结束了还没有返回,说明数组的每一位都是 9,比如 [9]、[9,9,9],加一之后数组会多出一位,结果是 [1,0]、[1,0,0,0],需要新建一个长度加一的数组来承载结果。

这个思路的关键点在于:只要遇到一个不是 9 的位置,加一操作就结束了。只有遇到 9 才需要继续往前看,而如果全是 9,那就直接构造一个“1 开头、后面全是 0”的新数组。

3.2 代码实现与逐行解读

先放 Python 版本:

class Solution: def plusOne(self, digits: list[int]) -> list[int]: n = len(digits) # 从最低位往最高位遍历 for i in range(n - 1, -1, -1): if digits[i] < 9: # 当前位不是9,加一后直接结束 digits[i] += 1 return digits # 当前位是9,置为0,进位继续往前传 digits[i] = 0 # 全部都是9,需要在最前面补一个1 return [1] + [0] * n

再看 Java 版本:

class Solution { 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; } int[] result = new int[n + 1]; result[0] = 1; return result; } }

两段代码的核心逻辑完全一样。我逐行说几个容易被忽视的细节:Python 里range(n - 1, -1, -1)表示从 n-1 一直循环到 0,注意第二参数是 -1,意思是“不包含 -1”,这样才能访问到下标 0。Java 里的 for 循环同理,i >= 0作为循环条件。千万不要写成i > 0,否则你会漏掉最高位。

3.3 为什么“提前返回”比“进位标志位”更优雅

我见过不少人的第一版代码是这样写的:

class Solution: def plusOne(self, digits: list[int]) -> list[int]: 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: break if carry == 1: digits.insert(0, 1) return digits

这种写法也能通过测试,而且在处理“连续进位”时逻辑也很直观。但我个人更喜欢“提前返回”的写法,原因有两个:第一,大部分输入的最后一位并不是 9,提前返回可以在第一轮循环就结束,不需要做模运算和除法,代码运行更快;第二,if digits[i] < 9的语义更贴近人脑做加法的直觉:“这位不是 9,加一就完事了”。相比之下,维护一个carry变量虽然通用,但代码不够精简,面试官在你写完代码后也更容易追问细节。

3.4 关于全 9 情况的“新数组”处理

当输入的数组是 [9]、[9,9]、[9,9,9] 这些全 9 的情况时,上面的 for 循环会把所有位置都置成 0,然后循环正常结束,走到最后一行。这时候需要一个全新的数组,长度比原来多 1,第一位是 1,其余位置是 0。比如 [9,9,9],加一之后变成 [1,0,0,0]。

在 Java 里,数组长度不可变,所以必须new int[n + 1]。int 数组默认初始化为 0,所以只需要给result[0] = 1赋值即可。在 Python 里,用[1] + [0] * n是最直观的写法,也有些写法是digits.insert(0, 1),但 insert 的时间复杂度是 O(n),与直接构造新数组一样,没必要为了省那几行代码牺牲可读性。

4. 复杂度分析与边界测试

4.1 时间复杂度与空间复杂度详解

这道题的时间复杂度是 O(n),n 是数组长度。最坏情况是输入全是 9,比如 [9,9,9,...,9],循环需要从头到尾把所有位都遍历一遍,把每一位变成 0,所以是 O(n)。最好情况是最后一位不是 9,循环只跑一次就返回,此时的时间复杂度接近 O(1)。平均情况介于两者之间,但由于现实中的输入大多数末位不是 9,实际运行速度非常快。

空间复杂度要分情况讨论。在“不全为 9”的情况下,我们没有申请任何与 n 相关的额外空间,是在原数组上直接修改,所以空间复杂度是 O(1)。在全 9 的情况下,需要新建一个长度为 n+1 的数组,额外空间是 O(n)。很多人写答案时会直接写“空间复杂度 O(1)”,这不严谨,准确的说法是“非全 9 时为 O(1),全 9 时为 O(n)”。

4.2 边界测试用例清单

做算法题一定要建立测试用例意识,尤其是这道题,边界情况比逻辑本身更容易出错。我整理了一份测试清单,建议刷题时对照着跑一遍:

输入数组输出数组对应场景
[1,2,3][1,2,4]普通情况,末位不进位
[4,3,2,1][4,3,2,2]普通情况,末位加一
[1,9][2,0]只有当前位是9,前一位收到进位后加一
[1,0,9][1,1,0]中间隔了0,进位在最高位前终止
[9][1,0]单元素全9,长度增加
[9,9,9][1,0,0,0]全9,长度增加,最经典边界
[0][1]输入本身就是0
[8,9,9][9,0,0]最高位是8,处理后变成9

每个用例背后代表的都是不同的进位路径。我最常看到有人漏测 [1,9] 和 [8,9,9],这两类用例恰好是“部分位是 9、但不是所有位都是 9”的情况。如果你只测了 [1,2,3] 和 [9,9,9],很容易写出一个“要么加一,要么全变 0”的错误逻辑。

4.3 Python 独有的 for-else 小技巧

Python 里有一种写法用到了 for-else 语法,for 循环正常结束(没有被 break 中断)时会执行 else 块。代码如下:

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 else: digits.insert(0, 1) return digits

这个写法的效果和前面的一样,for 循环因为在非 9 的位置提前 return,所以 else 块只在“所有位都是 9”的情况下执行。但习惯上,我更推荐直接return [1] + [0] * n或者 Java 里的new int[n+1],因为显式构造新数组的思路更通用,换到其他语言也能照搬。

5. 常见问题与易错点排查实录

5.1 易错点一:把数组转成数字,导致大数溢出

这个坑在文章开头已经详细说过,这里再补充一个现象:有很多用 Python 刷题的人,因为 int 没有溢出问题,就习惯于把数组转数字来做。这种解法在小数组上没问题,但一旦遇到大数组就会暴露出“和面试官预期不符”的问题。我建议从一开始就养成“直接在数组上操作”的习惯,不要把语言特性当成解题捷径。

5.2 易错点二:忘记处理全 9 情况

如果你写的代码里,for 循环结束后直接return digits,那么输入 [9] 时,你的代码会先把 digits[0] 变成 0,然后循环结束,返回 [0],这显然是错误的。正确做法是:循环结束后必须意识到“所有位置都已经被置为 0”,此时需要新建数组、第 0 位放 1。这是这道题最容易漏掉的部分,我建议在写代码时把最后一行当成“必然要写”的一行,不要觉得自己处理了循环就结束了。

5.3 易错点三:从前往后遍历

有些人习惯从下标 0 开始处理,但加法的进位方向是从低位到高位,也就是从数组末尾往开头移动。如果你从前往后,遇到 [9,9,9],先处理最高位,你会发现根本无法决定这个位置最后是 0 还是 1,因为你还没有看低位是否会给你进位。最终代码会越写越复杂,或者需要先遍历一遍数组判断是否全 9。记住一个原则:涉及到进位的模拟题,从低位开始处理,这是常识,也是避免混乱的根本方法。

5.4 易错点四:Java 里错误使用 insert 或 Arrays.copyOf

Java 的数组是定长的,没有 insert 方法。有些初学者会把数组转成ArrayList<Integer>,然后调用list.add(0, 1),再转回数组。这虽然可行,但涉及多次数据拷贝和装箱拆箱,代码量很大,没必要。全 9 时直接new int[n + 1],利用 int 数组默认全 0 的特性,只给首位赋 1,是最省事也最高效的写法。同理,不要用Arrays.copyOf(digits, n + 1),因为 copyOf 拷贝出来的数组是 [9,9,9,0] 这种形式,还需要额外处理前面的 9,思路会乱。

5.5 面试官爱问的两个变体

面试里经常会有两个追问方向。第一个是“如果题目改成加任意一个整数 k 呢”?这时候就不能只判断“是否为 9”了,而是要维护一个 carry,从低位开始,把 k 的每一位也考虑进来,类似“两数相加”的思路。第二个是“如果改成二进制数组,比如元素只有 0 和 1,加一怎么处理”?其实逻辑完全一样,只是进位条件从“等于 9”变成“等于 1”,判断阈值变了,代码骨架不用动。能把这两道变体顺畅写出来,说明你是真的理解了进位问题的本质。

6. 我在实际刷题中的体会与延伸

6.1 从这道题学到什么

我刷完这道题最大的体会是:算法题的基础从来不是背模板,而是把“数组模拟数字”这种抽象转化成代码能力。这个转化过程包括三个层面的能力:识别低位的方向、处理进位的条件、在边界情况下调整数据结构。这三个能力在这道题里全都有,所以它才会被各个公司当成高频热身题。

6.2 真实业务上的类比:版本号与计数器

别觉得这种“进位模拟”只在刷题时有用。实际开发里,版本号自增就是类似场景。比如一个软件版本号是 [2,1,9],要发布补丁变成 [2,2,0],这就是“加一”操作。再比如一些分布式系统里的序列号生成器,用数组或列表表示一个多段的计数器,当最低段达到上限时,归零并向上一段进位,逻辑和这道题如出一辙。理解了 66 题的进位思路,写这类业务代码时会顺畅很多。

6.3 推荐的同类型练习题目

如果你觉得这道题练得不过瘾,我推荐三道和它思路相似的题目:力扣 67 题“二进制求和”,本质上是把十进制加一扩展到两个二进制字符串相加;力扣 2 题“两数相加”,用链表低位在前的方式做多位数加法;力扣 415 题“字符串相加”,处理任意两个数字字符串的求和。把这几道题放在一起刷,你会发现它们共享同一个核心框架:从低位开始、逐位相加、记录进位、处理剩余位。66 题是这个框架最简单的一个实例,先把最简单的吃透,后面自然事半功倍。

最后再分享一个我自己的小习惯:每次刷完这种基础题,我会故意把代码写成两种风格,一种用提前返回,一种用 carry 标志位,然后对比两种写法的可读性和边界覆盖。这样做不是为了比谁优雅,而是为了在面试时遇到“面试官要求换一种写法”时,能从容切换。66 题虽然简单,但简单题里藏着的编程习惯,恰恰是别人一眼看出你代码功力的地方。

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

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

立即咨询