LeetCode 2. 两数相加:链表经典应用题详解

在LeetCode的入门到进阶路径中,第2题"两数相加"绝对是链表操作的必刷题------它不仅考察链表的遍历、节点创建等基础操作,还暗藏了"进位处理"的细节陷阱,是很多面试官偏爱用来考察基础功底的题目。今天就带大家从头到尾吃透这道题,从题目理解到代码实现,再到易错点规避,一步步讲清楚、讲明白。

一、题目解读(清晰易懂,避开文字陷阱)

题目核心很简单:给两个非空链表,这两个链表分别表示两个非负整数,但有两个关键规则需要注意:

  1. 链表的每位数字是逆序存储的。比如数字 342,用链表表示就是 2 → 4 → 3(因为逆序,链表头节点是个位,尾节点是最高位);

  2. 每个链表节点只能存一位数字(0-9);

  3. 特殊限制:除了数字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步走)

  1. 初始化变量:需要两个指针分别遍历两个输入链表(l1、l2),一个变量存储进位(carry,初始为0),一个虚拟头节点(或结果链表头节点)存储最终结果,还有一个指针(current)指向结果链表的当前节点,用于拼接新节点。

  2. 逐位相加遍历:循环遍历两个链表,直到两个链表都遍历完 并且 进位carry为0(这里很关键,避免遗漏最后一位进位,比如999+1=1000,最后进位1需要单独生成一个节点)。每一轮计算当前两位数字的和(sum = 链表1当前节点值 + 链表2当前节点值 + 进位carry)。

  3. 处理当前位和进位:计算当前位的实际值(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同时赋值),但加上兜底判断,代码会更健壮。

五、复杂度分析(面试必问)

对于算法题,除了代码实现,复杂度分析也是面试官重点关注的点,这道题的复杂度很直观,我们简单分析一下:

  1. 时间复杂度:O(max(m, n)),其中m和n分别是两个输入链表的长度。因为我们只需要遍历两个链表一次,循环次数取决于更长的那个链表的长度,再加上可能的一次进位循环(可忽略),所以时间复杂度是O(max(m, n))。

  2. 空间复杂度: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),该如何修改代码,实现两数相加?欢迎在评论区留言讨论~

相关推荐
芝加哥兔兔养殖场1 小时前
前端/iOS开发者必备工具软件合集
前端·ios
web打印社区1 小时前
web-print-pdf:专为Web打印而生的专业解决方案
前端·javascript·vue.js·electron·html
糖糖TANG1 小时前
学成在线 案例练习
前端·css
If using 10 days1 小时前
multiprocessing:创建并管理多个进程
python·算法
wu_asia2 小时前
每日一练壹
算法
程序员酥皮蛋2 小时前
hot 100 第二十二题 22.相交链表
数据结构·算法·leetcode·链表
全栈前端老曹2 小时前
【Redis】Redis 客户端连接与编程实践——Python/Java/Node.js 连接 Redis、实现计数器、缓存接口
前端·数据库·redis·python·缓存·全栈
午安~婉2 小时前
构图跟拍相关
前端·javascript·拍照·虚拟列表
一只小小的芙厨2 小时前
寒假集训·子集枚举2
c++·笔记·算法·动态规划