1. 链表两数相加问题解析
今天我想分享一道经典的LeetCode链表题目——两数相加(Add Two Numbers)。这道题编号为#2,在面试中出现的频率相当高。题目要求我们模拟两个数字相加的过程,只不过这两个数字是以链表形式逆序存储的。
举个例子,链表2->4->3表示数字342,链表5->6->4表示数字465。我们需要返回一个表示它们和(342+465=807)的链表7->0->8。看似简单,但实际处理起来有不少细节需要注意。
2. 初始思路与问题发现
2.1 我的第一个笨方法
最开始我采用了最直观的思路:先把两个链表转换成数字,相加后再转换回链表。具体实现是:
- 遍历链表,将每个节点的值存入动态数组
- 从数组末尾开始,计算每个位对应的数值(10的幂次)
- 将两个数字相加
- 把和转换为链表形式返回
java复制List<Integer> arr1 = new ArrayList<Integer>();
while(l1 != null){
arr1.add(l1.val);
l1 = l1.next;
}
// 同样的方法处理l2
// 然后计算sum1和sum2
long sum = sum1 + sum2;
// 最后将sum转换为链表
2.2 这个方法的问题
这个方法看似合理,但实际运行时会遇到几个严重问题:
- 整数溢出:当链表很长时,转换后的数字会超过long类型的最大值(2^63-1),导致计算结果错误
- 效率低下:需要进行多次幂运算和类型转换,时间复杂度高
- 空间浪费:需要额外的数组存储节点值
在LeetCode的测试用例中,当链表长度超过18位时,这个方法就会因为溢出而失败。
3. 优化思路与正确解法
3.1 逐位相加法
更合理的解法是模拟我们手工做加法的过程,逐位相加并处理进位。具体步骤:
- 初始化一个虚拟头节点(dummy head)和当前指针
- 同时遍历两个链表,对应位相加
- 处理进位(和≥10时需要进位)
- 创建新节点存储当前位的值
- 移动指针继续处理下一位
java复制ListNode head = new ListNode(-1); // 虚拟头结点
ListNode current = head;
int carry = 0; // 进位标志
3.2 循环条件分析
循环条件需要考虑三种情况:
- l1还有节点未处理
- l2还有节点未处理
- 还有未处理的进位
java复制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;
}
carry = sum / 10; // 计算新的进位
current.next = new ListNode(sum % 10); // 创建新节点
current = current.next; // 移动指针
}
3.3 虚拟头节点的妙用
这里使用虚拟头节点是一个很实用的技巧:
- 简化边界条件处理
- 避免单独处理第一个节点的特殊情况
- 最终返回head.next即可得到结果链表的真正头节点
4. 关键细节与注意事项
4.1 进位处理
进位处理是这道题最容易出错的地方:
- 每次相加后,carry = sum / 10
- 当前位的值是sum % 10
- 循环结束后还需要检查是否还有未处理的进位
4.2 链表长度不等的情况
两个链表长度可能不同,因此循环条件中使用||而不是&&:
- 当某个链表已经遍历完时,对应位的值视为0
- 这样可以统一处理不同长度的情况
4.3 时间复杂度分析
优化后的算法:
- 时间复杂度:O(max(m,n)),m和n分别是两个链表的长度
- 空间复杂度:O(max(m,n)),结果链表的长度最多为max(m,n)+1
5. 完整代码实现
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 dummyHead = new ListNode(-1); // 虚拟头节点
ListNode current = dummyHead;
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;
}
carry = sum / 10;
current.next = new ListNode(sum % 10);
current = current.next;
}
return dummyHead.next;
}
}
6. 常见错误与调试技巧
6.1 忘记处理最后的进位
比如999+1=1000,最后会产生一个额外的进位,如果不处理会导致结果错误。解决方法是在循环条件中加入carry != 0的判断。
6.2 指针移动错误
容易犯的错误是忘记移动l1和l2的指针,导致无限循环。确保每次处理完节点后都正确移动指针。
6.3 虚拟头节点处理不当
有些同学会直接使用真实头节点,这样需要额外处理第一个节点的特殊情况。使用虚拟头节点可以简化代码。
7. 测试用例建议
为了全面验证代码正确性,建议测试以下几种情况:
- 两个链表长度相同,无进位
- 输入:(2->4->3) + (5->6->4)
- 输出:7->0->8
- 两个链表长度不同
- 输入:(9->9) + (1)
- 输出:0->0->1
- 有连续进位
- 输入:(9->9->9) + (1)
- 输出:0->0->0->1
- 其中一个链表为空
- 输入:(1->2->3) + null
- 输出:1->2->3
8. 算法扩展思考
这道题还有一些变种值得思考:
- 如果链表存储的数字是正序的怎么办?(即1->2->3表示123)
- 如果允许修改原链表,能否实现O(1)空间复杂度的解法?
- 如何实现三个链表的相加?
对于第一个问题,可以考虑先反转链表,再用上述方法相加,最后再反转结果。或者使用栈来辅助处理。
在实际面试中,面试官可能会要求你同时实现正序和逆序两种情况的解法,因此理解这道题的各种变种很有必要。