2. 两数相加:链表模拟与进位处理详解
题目链接:2. 两数相加
题解类型:
text
链表模拟 / 逐位相加 / 进位处理
这题的核心不难:像手算加法一样,从个位开始逐位相加。
真正容易出错的地方是边界处理:
text
两条链表长度不同怎么办?
最后还有进位怎么办?
当前位和下一轮进位分别怎么计算?
结果链表的第一个节点怎么连接?
算法思路
题目使用倒序链表存储一个非负整数。
例如:
text
2 -> 4 -> 3
表示的数字是:
text
342
因为链表头部存的是个位:
text
个位 2 -> 十位 4 -> 百位 3
所以:
text
l1:2 -> 4 -> 3,表示 342
l2:5 -> 6 -> 4,表示 465
342 + 465 = 807
结果也要倒序存储:
text
7 -> 0 -> 8,表示 807
倒序存储让链表头部正好是个位,因此可以直接从 head 开始模拟手算加法。
1. 每一位到底算什么
假设当前轮:
text
l1 当前位:x
l2 当前位:y
上一位进位:carry
当前轮的总和是:
java
sum = x + y + carry;
其中:
text
sum % 10:当前结果节点保存的个位。
sum / 10:交给下一轮的进位。
例如:
text
9 + 8 + 0 = 17
因此:
text
当前结果位:17 % 10 = 7
下一轮进位:17 / 10 = 1
当前结果链表接入 7,并把 1 留给下一轮。
在本题中每一位最大是:
text
9 + 9 + 1 = 19
所以 carry 只可能是 0 或 1。
2. 完整推演:342 + 465
text
l1:2 -> 4 -> 3
l2:5 -> 6 -> 4
初始:
text
carry = 0
第 1 轮:个位
text
2 + 5 + 0 = 7
text
结果位:7 % 10 = 7
进位: 7 / 10 = 0
结果链表:
text
7
第 2 轮:十位
text
4 + 6 + 0 = 10
text
结果位:10 % 10 = 0
进位: 10 / 10 = 1
结果链表:
text
7 -> 0
第 3 轮:百位
text
3 + 4 + 1 = 8
text
结果位:8 % 10 = 8
进位: 8 / 10 = 0
结果链表:
text
7 -> 0 -> 8
两条链表都走完,且 carry == 0,结束。
3. 易错点一:两条链表长度不同,空位按 0 处理
例如:
text
l1:2 -> 4 -> 9,表示 942
l2:5 -> 6,表示 65
百位时,l2 已经没有节点,但不是停止计算,而是应该把缺失的一位当作 0:
text
9 + 0 + carry
所以每轮取值时写成:
java
int x = (l1 == null) ? 0 : l1.val;
int y = (l2 == null) ? 0 : l2.val;
含义是:
text
对应链表还有节点:取当前节点值。
对应链表已结束:当前位补 0。
不能因为其中一条链表结束,就直接停止循环;另一条链表的高位仍然需要放进结果。
4. 易错点二:两个链表结束后,进位可能还存在
例如:
text
l1:9 -> 9,表示 99
l2:1,表示 1
99 + 1 = 100
推演:
text
第 1 轮:9 + 1 + 0 = 10 -> 当前位 0,carry = 1
第 2 轮:9 + 0 + 1 = 10 -> 当前位 0,carry = 1
此时:
text
l1 == null
l2 == null
carry == 1
最高位的 1 仍必须创建为结果节点:
text
0 -> 0 -> 1
因此循环条件必须写为:
java
while (l1 != null || l2 != null || carry != 0)
三个条件的含义是:
text
l1 != null:l1 还有位没有处理。
l2 != null:l2 还有位没有处理。
carry != 0:还有最高位进位没有写入结果。
只要其中任何一个成立,就必须继续。
5. 易错点三:% 10 和 / 10 不能写反
假设:
text
sum = 17
正确写法:
java
int digit = sum % 10; // 7,当前节点保存的个位
carry = sum / 10; // 1,下一轮使用的进位
原因是十进制数 17 可以拆成:
text
17 = 1 * 10 + 7
其中:
text
7 留在当前位。
1 移交给更高一位。
因此不能写成:
java
tail.next = new ListNode(sum / 10);
carry = sum % 10;
那样会把进位与当前位完全弄反。
6. 为什么需要 dummy 和 tail
每一轮相加都会产生一个新的结果节点,因此需要不断向结果链表末尾追加节点。
先创建:
java
ListNode dummy = new ListNode(-1);
ListNode tail = dummy;
初始结构:
text
dummy -> null
^
tail
每算出一位:
java
tail.next = new ListNode(sum % 10);
tail = tail.next;
tail 始终指向结果链表当前最后一个节点。
dummy 是虚拟头节点,不属于答案;真正的结果从:
java
dummy.next
开始。
使用 dummy 的价值是:第一个结果节点和之后的结果节点可以使用同一套连接代码,不需要专门处理"结果链表还没有头节点"的情况。
7. Java 代码完整注释
java
class Solution {
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
// dummy 是结果链表的虚拟头节点。
// 它只用于统一连接操作,不属于最终结果。
ListNode dummy = new ListNode(-1);
// tail 始终指向结果链表当前最后一个节点。
ListNode tail = dummy;
// carry 保存上一位相加产生、需要加到当前位的进位。
int carry = 0;
// 只要任一链表还有节点,或还有最终进位,就继续计算。
while (l1 != null || l2 != null || carry != 0) {
// l1 已走完时,当前位按 0 处理。
int x = (l1 == null) ? 0 : l1.val;
// l2 已走完时,当前位按 0 处理。
int y = (l2 == null) ? 0 : l2.val;
// 当前位总和 = 两个当前位 + 低一位传来的进位。
int sum = x + y + carry;
// 当前结果节点保存个位。
tail.next = new ListNode(sum % 10);
// tail 移动到刚创建的结果节点。
tail = tail.next;
// 十位成为下一轮的进位。
carry = sum / 10;
// 还存在节点的链表向后移动一位。
if (l1 != null) {
l1 = l1.next;
}
if (l2 != null) {
l2 = l2.next;
}
}
// dummy 是辅助节点,真正的结果头节点是 dummy.next。
return dummy.next;
}
}
8. 易错点清单
| 易错点 | 错误后果 | 正确处理 |
|---|---|---|
| 一条链表结束就停止 | 另一条链表的高位丢失 | 空位按 0 处理 |
循环不判断 carry |
最终最高位进位丢失 | `while (l1 != null |
sum % 10 和 sum / 10 写反 |
当前结果位和进位错误 | % 10 写当前位,/ 10 写 carry |
未判断 l1、l2 是否为 null 就访问 .val |
空指针异常 | 三元表达式补 0 |
返回 dummy |
答案多出虚拟节点 -1 |
返回 dummy.next |
不移动 tail |
后续节点不能正确追加 | 每轮创建节点后 tail = tail.next |
写完代码后,至少用下面三类情况手动检查一次:
text
普通情况:342 + 465 = 807
长度不同:942 + 65 = 1007
最终进位:99 + 1 = 100
对应链表结果应分别是:
text
7 -> 0 -> 8
7 -> 0 -> 0 -> 1
0 -> 0 -> 1
9. 复杂度
假设两条链表长度分别为 m 和 n。
时间复杂度:
text
O(max(m, n))
每一位只处理一次,最多额外处理一次最终进位。
额外空间复杂度:
text
O(1)
这里不把题目要求返回的结果链表算入额外空间。除结果链表外,只使用了 dummy、tail、carry 等常数个变量。
一句话记忆:
text
当前位 = x + y + carry;结果节点存 sum % 10;下一轮进位是 sum / 10;空位补 0,最终进位也不能漏。