在链表题中,「两数相加」是一道非常经典的入门题。它同时考查了链表遍历、进位处理、哑节点以及边界条件,也是 Java 算法面试中经常出现的基础题。
这道题的代码不长,真正需要想清楚的是三个问题:为什么数字要逆序存储?两条链表长度不一致怎么办?遍历结束后为什么还要检查进位?
一、题目描述
给定两个非空链表 l1 和 l2,分别表示两个非负整数。每个节点只存储一位数字,并且数字按照逆序排列。
例如:
l1:2 → 4 → 3,表示 342
l2:5 → 6 → 4,表示 465
两数相加:
342 + 465 = 807
因此返回:
7 → 0 → 8
题目还保证:除了数字 0 本身之外,输入的数字不会以 0 开头。
二、为什么逆序存储更方便?
我们平时做加法时,总是从个位开始计算:
342
+ 465
-----
807
先计算个位 2 + 5,再计算十位 4 + 6,最后计算百位 3 + 4。如果某一位的和大于等于 10,还要把进位交给下一位。
题目中的链表头节点恰好就是个位,所以只需要从头到尾同时遍历两条链表,就能按照正常的加法顺序完成计算,不需要反转链表。
三、核心思路:逐位相加并保存进位
用两个指针分别遍历 l1 和 l2,同时维护变量 carry 表示进位。
每轮循环执行以下操作:
-
先把进位加入当前总和;
-
如果
l1还有节点,加上l1当前节点的值; -
如果
l2还有节点,加上l2当前节点的值; -
sum % 10是当前位应保存的数字; -
sum / 10是传给下一位的进位; -
创建新节点并接到结果链表末尾。
以 342 + 465 为例:
| 计算位置 | 当前计算 | 当前位 sum % 10 |
进位 sum / 10 |
|---|---|---|---|
| 个位 | 2 + 5 + 0 = 7 | 7 | 0 |
| 十位 | 4 + 6 + 0 = 10 | 0 | 1 |
| 百位 | 3 + 4 + 1 = 8 | 8 | 0 |
最终得到 7 → 0 → 8。
四、为什么循环条件要包含 carry?
循环条件是这道题最容易遗漏的地方:
while (p1 != null || p2 != null || carry != 0)
前两个条件用于处理两条链表长度不一致的情况,第三个条件用于处理最后仍有进位的情况。
例如:
l1:9 → 9,表示 99
l2:1,表示 1
计算结果为 100,对应链表:
0 → 0 → 1
当两条输入链表都遍历完时,carry 仍然等于 1。如果循环条件中没有 carry != 0,最后的节点 1 就会丢失。
五、完整 Java 代码
class Solution {
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
ListNode dummy = new ListNode(0);
ListNode current = dummy;
int carry = 0;
while (l1 != null || l2 != null || carry != 0) {
int sum = carry;
if (l1 != null) {
sum += l1.val;
l1 = l1.next;
}
if (l2 != null) {
sum += l2.val;
l2 = l2.next;
}
int digit = sum % 10;
carry = sum / 10;
current.next = new ListNode(digit);
current = current.next;
}
return dummy.next;
}
}
六、为什么要使用哑节点?
代码中的 dummy 是一个不保存有效结果的哑节点:
ListNode dummy = new ListNode(0);
ListNode current = dummy;
如果不使用哑节点,创建第一个结果节点时需要单独判断链表头是否为空。使用哑节点后,每次都可以统一执行:
current.next = new ListNode(digit);
current = current.next;
最后返回 dummy.next,跳过哑节点即可。哑节点并不是这道题特有的技巧,在链表合并、删除节点、分区等问题中也非常常见。
七、截图代码中的一个细节
截图代码开头写了:
if (l1 == null && l2 == null) {
return null;
}
根据题目描述,l1 和 l2 都是非空链表,因此这个判断不是必需的。保留它不会影响运行,但按题目约束可以直接省略,让核心逻辑更简洁。
另外,变量名 dum、p 和 temp 可以分别改成 dummy、current 和 digit。算法不变,但语义更加清楚,也更符合面试和工程代码的可读性要求。
八、常见错误
1. 忘记处理最后的进位
只写下面的条件:
while (l1 != null || l2 != null)
会让 99 + 1 丢失最后的 1。正确条件应包含 carry != 0。
2. 默认两条链表长度相同
两条链表可能分别表示 12345 和 67,因此访问节点值之前必须先判断节点是否为空。
3. 混淆当前位与进位
int digit = sum % 10; // 当前节点保存的数字
carry = sum / 10; // 下一位使用的进位
在十进制加法中,二者不要写反。
4. 返回 dummy 而不是 dummy.next
dummy 只是辅助节点,不属于真正结果。返回值必须是:
return dummy.next;
九、复杂度分析
假设两条链表长度分别为 m 和 n:
-
时间复杂度:
O(max(m, n)),两条链表最多各遍历一次; -
空间复杂度:
O(max(m, n)),用于保存结果链表。
如果不计算返回结果本身占用的空间,额外空间复杂度是 O(1)。