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

相关推荐
梦帮科技2 分钟前
Web 漏洞攻防深度:WAF 规则逃逸技术(畸形分块传输、双重编码)与对抗加固
linux·前端·网络安全·ssl·web
兰亭妙微UI设计公司9 分钟前
兰亭妙微软件界面设计公司的解锁按钮设计10大密码
开发语言·前端·javascript
zx_7414848124 分钟前
YOLOv3 核心原理详解:Darknet-53 架构、多尺度预测与 FPN 特征融合
算法·yolo·目标检测·yolov3
MoRanzhi120324 分钟前
122.四平方和(暴力枚举)
python·数学·算法·蓝桥杯·完全平方数·暴力枚举·四平方和定理
倒头就睡的小比特25 分钟前
贪心算法介绍
算法·贪心算法
迪幻32 分钟前
封装自己的 npm 依赖并存储到个人 GitHub
前端·npm·github
u0111026751 小时前
WebP 转成 PNG 后为什么更大?图片格式转换不等于压缩
前端·javascript·vue.js
万少1 小时前
Qoder sonus模型 20分钟,复刻 马斯克 飞天汽车
前端·后端·算法
七牛云行业应用1 小时前
Muse Spark Open 发布:Meta 称其为美国最强开放权重模型,816B 参数如何落地
算法
java1234_小锋1 小时前
【技术专题】HTML5 网页技术 - HTML5 列表标签
前端·html·html5