在LeetCode的入门到进阶路径中,第2题"两数相加"绝对是链表操作的必刷题------它不仅考察链表的遍历、节点创建等基础操作,还暗藏了"进位处理"的细节陷阱,是很多面试官偏爱用来考察基础功底的题目。今天就带大家从头到尾吃透这道题,从题目理解到代码实现,再到易错点规避,一步步讲清楚、讲明白。
一、题目解读(清晰易懂,避开文字陷阱)
题目核心很简单:给两个非空链表,这两个链表分别表示两个非负整数,但有两个关键规则需要注意:
-
链表的每位数字是逆序存储的。比如数字 342,用链表表示就是 2 → 4 → 3(因为逆序,链表头节点是个位,尾节点是最高位);
-
每个链表节点只能存一位数字(0-9);
-
特殊限制:除了数字0本身,两个输入链表都不会以0开头(也就是说,不会出现 0 → 1 → 2 这种表示 210 的无效情况)。
我们的任务:将这两个链表表示的整数相加,结果也用同样的"逆序链表"形式返回。
举个例子,快速理解
示例1:
输入:l1 = [2,4,3](表示 342),l2 = [5,6,4](表示 465)
输出:[7,0,8](表示 807)
解释:342 + 465 = 807,逆序存储后就是 7 → 0 → 8,和输出链表一致。
示例2(包含进位和链表长度不一致):
输入:l1 = [9,9,9,9,9,9,9](表示 9999999),l2 = [9,9,9,9](表示 9999)
输出:[8,9,9,9,0,0,0,1](表示 10009998)
这个例子能覆盖大部分边界情况,后面会重点讲解如何处理。
二、解题思路(核心:模拟竖式加法,处理进位)
这道题的本质的是"模拟我们小学学的竖式加法",只不过数字是逆序存储在链表中,反而给我们的操作带来了便利------因为竖式加法是从个位开始相加,而链表的头节点正好是个位,我们可以直接从两个链表的头节点开始遍历,逐位相加即可。
核心思路拆解(3步走)
-
初始化变量:需要两个指针分别遍历两个输入链表(l1、l2),一个变量存储进位(carry,初始为0),一个虚拟头节点(或结果链表头节点)存储最终结果,还有一个指针(current)指向结果链表的当前节点,用于拼接新节点。
-
逐位相加遍历:循环遍历两个链表,直到两个链表都遍历完 并且 进位carry为0(这里很关键,避免遗漏最后一位进位,比如999+1=1000,最后进位1需要单独生成一个节点)。每一轮计算当前两位数字的和(sum = 链表1当前节点值 + 链表2当前节点值 + 进位carry)。
-
处理当前位和进位:计算当前位的实际值(sum % 10),创建新节点拼接到结果链表中;更新进位(carry = Math.floor(sum / 10)),然后移动两个输入链表的指针和结果链表的current指针,继续下一轮循环。
为什么不用先把链表转成数字再相加?
很多新手会想到:把两个链表转成整数,相加后再转成逆序链表。但这种方法有一个致命问题------整数溢出。
LeetCode中链表的长度可能很长(比如超过100个节点),对应的数字会远超JavaScript中Number的最大安全值(2^53 - 1),此时转成数字会丢失精度,导致结果错误。因此,必须用"逐位相加"的方式,避免溢出问题。
三、完整代码实现(TypeScript版,可直接运行)
题目中已经给出了ListNode的类定义和函数签名,我们直接基于此实现,同时添加详细注释,每一步都讲清楚用途,方便大家理解和复用。
typescript
// 链表节点类定义(题目已给出,无需修改)
class ListNode {
val: number
next: ListNode | null
constructor(val?: number, next?: ListNode | null) {
// 节点值默认是0(如果未传入)
this.val = (val === undefined ? 0 : val)
// 节点的下一个节点默认是null(如果未传入)
this.next = (next === undefined ? null : next)
}
}
/**
* 两数相加核心函数
* @param l1 第一个逆序链表(表示非负整数)
* @param l2 第二个逆序链表(表示非负整数)
* @returns 相加结果的逆序链表
*/
function addTwoNumbers(l1: ListNode | null, l2: ListNode | null): ListNode | null {
// 定义两个指针,分别遍历l1和l2(避免修改原链表)
let num1 = l1;
let num2 = l2;
// 进位变量,初始为0(没有进位)
let carry = 0;
// 结果链表的头节点(初始为null,后续创建第一个节点后赋值)
let result = null;
// 指向结果链表当前节点的指针,用于拼接新节点
let current = null;
// 循环条件:只要有一个链表没遍历完,或者还有进位,就继续循环
while (num1 || num2 || carry) {
// 计算当前位的和:num1的当前值(没有则为0) + num2的当前值(没有则为0) + 进位
let sum = (num1 ? num1.val : 0) + (num2 ? num2.val : 0) + carry;
// 更新进位:sum除以10取整(比如sum=15,进位就是1;sum=7,进位就是0)
carry = Math.floor(sum / 10);
// 创建当前位的节点(sum取余10,得到当前位的实际数字)
const node = new ListNode((sum % 10));
// 第一次创建节点时,初始化结果链表的头节点和current指针
if (!result) {
result = node;
current = node;
} else {
// 非第一次创建节点,拼接在current的后面,并移动current指针
if (!current) return null; // 兜底判断(理论上不会触发,避免ts报错)
current.next = node;
current = node;
}
// 移动num1和num2指针(如果当前节点存在,才移动到下一个)
if (num1) num1 = num1.next;
if (num2) num2 = num2.next;
}
// 循环结束,返回结果链表的头节点
return result;
};
四、代码逐行解析(重点突破易错点)
上面的代码看似简单,但有几个细节很容易出错,我们逐行拆解关键部分,帮大家避开陷阱。
1. 指针初始化(num1、num2)
我们没有直接修改l1和l2,而是用num1 = l1、num2 = l2作为遍历指针------这是一个良好的编程习惯,避免破坏原输入链表的数据,后续如果需要复用原链表,还能正常使用。
2. 循环条件(while (num1 || num2 || carry))
这是最容易出错的地方之一!很多新手会写成 while (num1 || num2),这样会遗漏"最后一位进位"的情况。
比如:l1 = [9],l2 = [9],相加后sum=18,carry=1,循环结束后carry=1,此时需要再创建一个节点(值为1),否则结果会少一位(变成[8],而正确结果是[8,1])。
因此,必须加上|| carry,确保进位处理完毕。
3. 当前位和计算(sum = (num1 ? num1.val : 0) + ...)
两个链表的长度可能不一致(比如l1有3个节点,l2有5个节点),此时遍历到l1的末尾(num1为null),我们就用0代替它的节点值,避免报错,同时不影响计算结果。
4. 结果链表的拼接(result和current指针)
result指针用于保存结果链表的头节点(最终需要返回它),current指针用于"移动拼接"新节点------如果只用电result指针,拼接新节点后会丢失头节点的位置,无法返回正确结果。
第一次创建节点时(result为null),将result和current都指向这个新节点;后续每次创建节点,都拼接到current.next,然后current移动到current.next,确保链表连续。
5. 兜底判断(if (!current) return null)
这一步是为了避免TypeScript的类型报错------理论上,只要result不为null,current就不会为null(因为第一次创建节点时,current和result同时赋值),但加上兜底判断,代码会更健壮。
五、复杂度分析(面试必问)
对于算法题,除了代码实现,复杂度分析也是面试官重点关注的点,这道题的复杂度很直观,我们简单分析一下:
-
时间复杂度:O(max(m, n)),其中m和n分别是两个输入链表的长度。因为我们只需要遍历两个链表一次,循环次数取决于更长的那个链表的长度,再加上可能的一次进位循环(可忽略),所以时间复杂度是O(max(m, n))。
-
空间复杂度:O(max(m, n)) 或 O(max(m, n) + 1)。结果链表的长度最多比更长的输入链表多1(比如999+1=1000,长度从3变成4),因此空间复杂度和输入链表的最长长度成正比。
注意:如果题目允许"修改原链表"来存储结果,可以将空间复杂度优化到O(1),但这种做法会破坏原输入数据,一般不推荐(除非题目明确要求)。
六、常见错误总结(避坑指南)
结合平时刷题和面试中遇到的情况,总结几个新手最容易犯的错误,大家可以对照检查:
-
循环条件遗漏carry:导致最后一位进位无法处理,结果少一位;
-
链表长度不一致时,未用0补位:遍历到短链表末尾时,直接停止遍历,导致后续位数漏加;
-
修改原输入链表:直接用l1、l2作为遍历指针,后续无法复用原链表,虽然不影响这道题的结果,但属于不良编程习惯;
-
忘记移动指针:遍历完当前节点后,未移动num1、num2或current指针,导致死循环;
-
忽略整数溢出:试图将链表转成数字再相加,导致长链表对应的数字精度丢失,结果错误。
七、总结与拓展
"两数相加"这道题,看似是链表题,本质是"模拟竖式加法",核心在于进位处理 和链表遍历拼接。只要掌握了"逐位相加、保留当前位、更新进位、拼接节点"这四个关键步骤,就能轻松解决这道题。
这道题是链表操作的入门题,后续很多复杂的链表题(比如两数相加II,链表逆序后再相加),都是基于这道题的思路拓展而来。建议大家多动手写代码,多测试边界情况,把基础打牢,后续面对更复杂的算法题时,才能游刃有余。
最后,留给大家一个小思考:如果输入的链表是正序存储的(比如342表示为3→4→2),该如何修改代码,实现两数相加?欢迎在评论区留言讨论~