🔢 两数相加:链表界的“列竖式”大法
摘要:本文详解 LeetCode 2 题“两数相加”,通过“虚拟头节点 + 进位模拟”实现链表加法,深入剖析逆序存储的巧妙逻辑,助你掌握链表操作的核心技巧。
📚 核心知识点:链表模拟加法
这道题是链表操作的经典之作,核心在于模拟手工列竖式计算的过程。与数组不同,链表的逆序存储(低位在前,高位在后)完美契合了加法从低位向高位进位的计算顺序。
关键概念:
- 虚拟头节点(Dummy Node) :创建一个临时节点作为结果链表的起点,避免了对头节点的特殊判断,让代码逻辑更统一、更优雅。
- 进位处理(Carry) :使用变量记录每一位相加后的进位值,传递给下一位计算。
- 时间与空间复杂度:时间复杂度为 O(max(m,n))(m、n 为链表长度),空间复杂度为 O(1)(除结果链表外无额外空间)。
📝 题目解析:LeetCode 2. 两数相加
题目描述:
给你两个非空的链表,分别用来表示两个非负整数。其中,它们各自的位数是按照逆序的方式存储的,并且它们的每个节点只能存储一位数字。请你将这两个数相加,并以相同形式返回一个表示和的链表。
示例:
输入:l1 = [2,4,3](代表数字 342),l2 = [5,6,4](代表数字 465)
输出:[7,0,8](代表数字 807,因为 342 + 465 = 807)
🚀 解题思路:像拉链一样计算
核心观察:
因为链表是逆序存储的,所以链表的头节点就是数字的个位。这和我们手工做加法“从个位开始加”的顺序完全一致,无需反转链表!
解题步骤:
-
初始化:创建虚拟头节点
dummy和遍历指针cur,初始化进位变量carry = 0。 -
遍历与计算:同时遍历两个链表:
- 取出当前节点的值(如果链表已遍历完则取 0)。
- 计算当前位的和:
sum_val = x + y + carry。 - 计算当前位的结果:
sum_val % 10。 - 更新进位:
sum_val // 10。 - 创建新节点,接到结果链表,并移动指针。
-
处理最终进位:如果遍历结束后还有进位(
carry > 0),需要额外创建一个节点存储该进位。 -
返回结果:返回
dummy.next。
💻 代码实现(Python,带详细注释)
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def addTwoNumbers(self, l1: Optional[ListNode], l2: Optional[ListNode]) -> Optional[ListNode]:
# 1. 创建虚拟头节点,统一结果链表的创建逻辑
dummy = ListNode(-1)
cur = dummy # 结果链表的遍历指针
carry = 0 # 进位,初始为0
# 2. 遍历两个链表,直到都遍历完
while l1 or l2:
# 取当前节点的值,链表遍历完则取0
x = l1.val if l1 else 0
y = l2.val if l2 else 0
# 3. 计算当前位的和(包含进位)
sum_val = x + y + carry
# 当前位的结果:取模10
current = sum_val % 10
# 新的进位:整除10
carry = sum_val // 10
# 4. 创建新节点,接到结果链表
cur.next = ListNode(current)
cur = cur.next # 结果指针后移
# 5. 移动l1、l2指针(如果不为空)
if l1:
l1 = l1.next
if l2:
l2 = l2.next
# 6. 处理最终进位:如果还有进位,额外创建节点
if carry > 0:
cur.next = ListNode(carry)
# 7. 返回虚拟头的下一个节点(真正的结果链表头)
return dummy.next
🧪 代码逐行演示(以示例 1 为例)
输入:
l1 = 2 → 4 → 3(代表 342)l2 = 5 → 6 → 4(代表 465)- 预期结果:
7 → 0 → 8(代表 807)
| 步骤 | l1值 | l2值 | 进位carry | sum_val | 当前位结果 | 新进位 | 链表状态 |
|---|---|---|---|---|---|---|---|
| 1 | 2 | 5 | 0 | 7 | 7 | 0 | dummy → 7 |
| 2 | 4 | 6 | 0 | 10 | 0 | 1 | dummy → 7 → 0 |
| 3 | 3 | 4 | 1 | 8 | 8 | 0 | dummy → 7 → 0 → 8 |
| 4 | None | None | 0 | - | - | - | 循环结束,无额外进位 |
最终结果:7 → 0 → 8 ✅(对应 807,342+465=807)
⚠️ 关键细节与边界情况
-
链表长度不一致:
- 例如
l1 = 9→9→9→9→9→9→9,l2 = 9→9→9→9。 - 处理逻辑:在遍历较短链表时,将其值视为 0,继续与较长链表的剩余部分相加。
- 例如
-
最终进位处理:
- 例如
l1 = 9→9,l2 = 9→9。 - 相加后得到
8→9,但此时进位1不能被忽略。循环结束后,必须额外创建一个节点1,结果为8→9→1。
- 例如
-
虚拟头节点的作用:
- 它像一个“占位符”,让我们不需要单独处理第一个节点的创建,所有节点的插入逻辑都是一致的,极大降低了代码出错的概率。
📊 复杂度分析
| 复杂度类型 | 分析结果 | 说明 |
|---|---|---|
| 时间复杂度 | O(max(m,n)) | 只需要遍历较长链表的长度即可完成计算。 |
| 空间复杂度 | O(1) | 除了返回的结果链表外,只使用了常数个额外变量(指针、进位值等)。 |
🚀 进阶与同类题拓展
掌握了这个“模拟加法 + 进位 + 虚拟头”的模板,你可以轻松解决以下变种问题:
-
🔹 445. 两数相加 II(正序存储):
- 差异:数字最高位在链表头。此时无法直接从低位开始加。
- 解法:需要先反转链表,或者使用栈来倒序处理数据。
-
🔹 67. 二进制求和(字符串模拟):
- 差异:输入是二进制字符串。
- 解法:思路完全一致,只是进位规则变成了“逢二进一”。
-
🔹 989. 数组形式的整数加法(数组模拟):
- 差异:输入是数组。
- 解法:同样适用模拟加法的思路,从数组末尾开始遍历。
📌 总结
- 核心思想:利用链表逆序存储的特性,模拟手工列竖式加法,配合虚拟头节点构建结果链表。
- 避坑指南:千万别忘了处理最后的进位,也别忘了处理长短不一的情况。
- 通用模板:这种“双指针遍历 + 进位变量 + 虚拟头节点”的模式,是解决所有“模拟加法”类问题的通用解法。
希望这篇文章能帮你彻底拿下这道经典面试题!如果有任何疑问,欢迎在评论区留言讨论。我是 爱摸鱼的打工仔,我们下期再见!🐟