2. 两数相加
❌|✅|💡|📌 问题简介
给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。
请你将两个数相加,并以相同形式返回一个表示和的链表。
你可以假设除了数字 0 之外,这两个数都不会以 0 开头。
题目描述
- 输入:两个单链表
l1和l2 - 输出:一个新的单链表,表示
l1和l2所代表数字之和 - 约束:
- 每个链表节点值为 0-9
- 链表按逆序存储(个位在头)
- 不会以 0 开头(除了数字 0 本身)
📌 示例说明
示例 1:
输入:l1 = [2,4,3], l2 = [5,6,4]
输出:[7,0,8]
解释:342 + 465 = 807
示例 2:
输入:l1 = [0], l2 = [0]
输出:[0]
示例 3:
输入:l1 = [9,9,9,9,9,9,9], l2 = [9,9,9,9]
输出:[8,9,9,9,0,0,0,1]
💡 解题思路
方法一:模拟加法(推荐)
这是最直观的方法,直接模拟我们手算加法的过程:
-
初始化:
- 创建一个虚拟头节点
dummy,方便处理结果链表 - 设置指针
curr指向当前要处理的位置 - 设置进位
carry = 0
- 创建一个虚拟头节点
-
循环处理:
- 当
l1或l2还有节点,或者还有进位时,继续循环 - 获取当前位的值:如果链表还有节点就取值,否则为 0
- 计算当前位的和:
sum = val1 + val2 + carry - 更新进位:
carry = sum / 10 - 创建新节点:
sum % 10 - 移动指针到下一位
- 当
-
返回结果:
- 返回
dummy.next(跳过虚拟头节点)
- 返回
方法二:递归解法
也可以使用递归来解决这个问题:
- 基础情况:如果两个链表都为空且没有进位,返回 null
- 递归情况 :
- 计算当前位的和
- 创建当前节点
- 递归处理剩余部分
- 连接当前节点和递归结果
方法三:先转换为数字再相加(不推荐)
理论上可以将链表转换为整数,相加后再转回链表,但这种方法有以下问题:
- 可能导致整数溢出(题目中链表可能很长)
- 违背了链表操作的本质
- 时间复杂度更高
因此不推荐使用方法三。
💻 代码实现
=== "Java"
java
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
ListNode dummy = new ListNode(0);
ListNode curr = dummy;
int carry = 0;
while (l1 != null || l2 != null || carry != 0) {
int val1 = (l1 != null) ? l1.val : 0;
int val2 = (l2 != null) ? l2.val : 0;
int sum = val1 + val2 + carry;
carry = sum / 10;
curr.next = new ListNode(sum % 10);
curr = curr.next;
if (l1 != null) l1 = l1.next;
if (l2 != null) l2 = l2.next;
}
return dummy.next;
}
}
=== "Go"
go
/**
* Definition for singly-linked list.
* type ListNode struct {
* Val int
* Next *ListNode
* }
*/
func addTwoNumbers(l1 *ListNode, l2 *ListNode) *ListNode {
dummy := &ListNode{Val: 0}
curr := dummy
carry := 0
for l1 != nil || l2 != nil || carry != 0 {
val1 := 0
if l1 != nil {
val1 = l1.Val
l1 = l1.Next
}
val2 := 0
if l2 != nil {
val2 = l2.Val
l2 = l2.Next
}
sum := val1 + val2 + carry
carry = sum / 10
curr.Next = &ListNode{Val: sum % 10}
curr = curr.Next
}
return dummy.Next
}
📊 示例演示
让我们用示例 1 来演示算法执行过程:
l1 = [2,4,3] // 表示 342
l2 = [5,6,4] // 表示 465
步骤 1: 2 + 5 + 0 = 7, carry = 0 → [7]
步骤 2: 4 + 6 + 0 = 10, carry = 1 → [7,0]
步骤 3: 3 + 4 + 1 = 8, carry = 0 → [7,0,8]
步骤 4: 两个链表都结束,carry = 0,结束
结果: [7,0,8] // 表示 807
对于示例 3 的演示:
l1 = [9,9,9,9,9,9,9]
l2 = [9,9,9,9]
步骤 1: 9 + 9 + 0 = 18, carry = 1 → [8]
步骤 2: 9 + 9 + 1 = 19, carry = 1 → [8,9]
步骤 3: 9 + 9 + 1 = 19, carry = 1 → [8,9,9]
步骤 4: 9 + 9 + 1 = 19, carry = 1 → [8,9,9,9]
步骤 5: 9 + 0 + 1 = 10, carry = 1 → [8,9,9,9,0]
步骤 6: 9 + 0 + 1 = 10, carry = 1 → [8,9,9,9,0,0]
步骤 7: 9 + 0 + 1 = 10, carry = 1 → [8,9,9,9,0,0,0]
步骤 8: 0 + 0 + 1 = 1, carry = 0 → [8,9,9,9,0,0,0,1]
✅ 答案有效性证明
我们的解法是正确的,原因如下:
- 正确处理进位:每次计算都考虑了上一位的进位,并正确计算当前位的进位
- 处理不同长度:当一个链表比另一个短时,我们将其后续位视为 0
- 处理最终进位:即使两个链表都处理完了,如果有进位仍会创建新节点
- 保持逆序:由于输入是逆序的,我们按顺序处理每一位,结果自然也是逆序的
数学归纳法证明:
- 基础情况:当两个链表都为空且无进位时,返回空链表,正确
- 归纳假设:假设前 k 位计算正确
- 归纳步骤:第 k+1 位的计算基于前 k 位的进位,按照十进制加法规则,必然正确
📈 复杂度分析
| 复杂度类型 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 分析 | O(max(m,n)) | O(max(m,n)) |
| 说明 | m, n 分别为两个链表的长度,需要遍历较长的链表 | 结果链表的长度最多为 max(m,n)+1 |
- 时间复杂度:O(max(m,n)),其中 m 和 n 分别是两个链表的长度。我们需要遍历两个链表的每一位。
- 空间复杂度:O(max(m,n)),用于存储结果链表。如果不考虑输出空间,额外空间复杂度为 O(1)。
📌 问题总结
✅ 关键要点:
- 理解链表逆序存储的含义(个位在头部)
- 正确处理进位逻辑
- 使用虚拟头节点简化边界情况处理
- 考虑链表长度不一致的情况
✅ 常见陷阱:
- 忘记处理最后的进位(如 5+5=10 的情况)
- 没有正确处理链表长度不同的情况
- 在移动指针时出现空指针异常
✅ 扩展思考:
- 如果链表是正序存储(高位在前),该如何处理?(需要使用栈或递归)
- 如何处理负数的情况?
- 如果要实现链表的乘法,思路是什么?
github地址: https://github.com/swf2020/LeetCode-Hot100-Solutions