力扣解题-2. 两数相加
给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。
请你将两个数相加,并以相同形式返回一个表示和的链表。
你可以假设除了数字 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]
提示:
每个链表中的节点数在范围 [1, 100] 内
0 <= Node.val <= 9
题目数据保证列表表示的数字不含前导零
Related Topics
递归、链表、数学
第一次解答
解题思路
核心方法:模拟手工加法(迭代版),通过哑节点(dummyHead)简化结果链表的头节点处理,逐位计算两个链表对应节点的和并处理进位,循环直到两个链表遍历完毕且无进位,时间复杂度O(max(n,m))、空间复杂度O(max(n,m)),是本题的经典最优解法。
核心逻辑拆解
链表逆序存储数字的特性,恰好匹配手工加法“从个位开始计算”的逻辑:
- 哑节点初始化:创建
dummyHead(哑节点)作为结果链表的虚拟头节点,current指针指向哑节点,用于构建结果链表; - 进位初始化:
carry表示加法进位(初始为0,因为个位相加无初始进位); - 循环计算(核心):
- 循环条件:
l1 != null || l2 != null || carry != 0(只要有链表未遍历完,或仍有进位,就需要继续计算); - 取当前位数值:
val1为l1当前节点值(l1为空则取0),val2为l2当前节点值(l2为空则取0); - 计算当前位总和:
sum = val1 + val2 + carry; - 更新进位:
carry = sum / 10(总和≥10时进位为1,否则为0); - 计算当前位结果:
digit = sum % 10(取余数作为当前位数字); - 构建结果节点:
current.next指向新创建的digit节点,current移动到下一个节点; - 移动链表指针:l1、l2非空时分别向后移动;
- 循环条件:
- 返回结果:返回
dummyHead.next(跳过哑节点,得到结果链表的真实头节点)。
具体步骤(以示例1 l1=[2,4,3]、l2=[5,6,4]为例)
| 步骤 | l1值 | l2值 | carry | sum | digit | 结果链表(current指向) | 说明 |
|---|---|---|---|---|---|---|---|
| 1 | 2 | 5 | 0 | 7 | 7 | dummyHead→7 | 个位相加:2+5+0=7,无进位 |
| 2 | 4 | 6 | 0 | 10 | 0 | 7→0 | 十位相加:4+6+0=10,进位1 |
| 3 | 3 | 4 | 1 | 8 | 8 | 0→8 | 百位相加:3+4+1=8,无进位 |
| 4 | null | null | 0 | - | - | - | 循环终止 |
| 最终结果链表:7→0→8,与示例一致。 |
性能说明
- 时间复杂度:O(max(n,m))(n、m分别为两个链表的长度,最多遍历较长链表的长度+1次,处理最后一位进位);
- 空间复杂度:O(max(n,m))(结果链表的长度最多为max(n,m)+1,如示例3的7位+4位=8位);
- 优势:
- 哑节点避免了头节点的特殊处理(无需判断结果链表是否为空);
- 循环条件覆盖了“链表长度不一致”和“最后一位进位”的边界场景;
- 逐位计算符合数学加法逻辑,无冗余操作,执行效率极高。
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
ListNode dummyHead = new ListNode(0);
ListNode current=dummyHead;
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;
int digit=sum%10;
current.next=new ListNode(digit);
current=current.next;
if(l1!=null){
l1=l1.next;
}
if(l2!=null) {
l2 = l2.next;
}
}
return dummyHead.next;
}
示例解答
解题思路
解法1:递归版(逻辑等价,代码更简洁)
核心方法:递归模拟加法,将迭代的循环逻辑转化为递归调用,每一层递归处理一位数字的相加和进位传递,逻辑更简洁但递归深度受限于链表长度(最多101层,无栈溢出风险)。
核心逻辑
递归的核心是“处理当前位 + 递归处理剩余位 + 传递进位”:
- 递归终止条件:l1为空 && l2为空 && carry=0,返回null;
- 计算当前位数值:val1=l1.val(空则0),val2=l2.val(空则0);
- 计算sum和carry,创建当前位结果节点;
- 递归调用处理下一位(l1.next/l2.next,传递新的carry),作为当前节点的next;
- 返回当前节点。
代码实现
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
return add(l1, l2, 0);
}
// 辅助递归函数:l1、l2为当前处理节点,carry为进位
private ListNode add(ListNode l1, ListNode l2, int carry) {
// 递归终止:无节点且无进位
if (l1 == null && l2 == null && carry == 0) {
return null;
}
// 取当前位值
int val1 = l1 != null ? l1.val : 0;
int val2 = l2 != null ? l2.val : 0;
// 计算当前位和进位
int sum = val1 + val2 + carry;
int newCarry = sum / 10;
int digit = sum % 10;
// 创建当前节点,递归处理下一位
ListNode node = new ListNode(digit);
node.next = add(
l1 != null ? l1.next : null,
l2 != null ? l2.next : null,
newCarry
);
return node;
}
优势与适用场景
- 时间复杂度:O(max(n,m)),与迭代版一致;
- 空间复杂度:O(max(n,m))(递归调用栈的深度=结果链表长度);
- 优势:代码更简洁,符合“分治”的编程思维,无需手动管理指针移动;
- 注意事项:链表长度≤100,递归深度≤101,不会触发栈溢出(Java默认栈深度约1000)。
解法2:空间优化版(复用原链表节点,进阶思路)
核心方法:复用较长的原链表节点存储结果,避免创建新节点,减少内存分配开销,空间复杂度仍为O(max(n,m))(最坏情况仍需创建最后一位进位节点),但实际内存占用更低。
代码实现
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
ListNode dummy = new ListNode(0);
ListNode curr = dummy;
int carry = 0;
ListNode p1 = l1, p2 = l2;
// 先遍历两个链表都有的部分,复用节点
while (p1 != null && p2 != null) {
int sum = p1.val + p2.val + carry;
carry = sum / 10;
p1.val = sum % 10; // 复用l1的节点存储结果
curr.next = p1;
curr = curr.next;
p1 = p1.next;
p2 = p2.next;
}
// 处理l1剩余节点
while (p1 != null) {
int sum = p1.val + carry;
carry = sum / 10;
p1.val = sum % 10;
curr.next = p1;
curr = curr.next;
p1 = p1.next;
}
// 处理l2剩余节点
while (p2 != null) {
int sum = p2.val + carry;
carry = sum / 10;
p2.val = sum % 10;
curr.next = p2;
curr = curr.next;
p2 = p2.next;
}
// 处理最后的进位
if (carry > 0) {
curr.next = new ListNode(carry);
}
return dummy.next;
}
优势说明
- 减少新节点创建:复用原链表的节点,仅在最后有进位时创建新节点;
- 性能优化:减少对象创建和内存分配的开销,执行效率略高于迭代版;
- 局限性:修改了原链表的节点值,若需要保留原链表数据则不适用。
总结
- 迭代版(第一次解答):逻辑清晰、无副作用(不修改原链表),O(max(n,m))时间+O(max(n,m))空间,是工程首选的经典解法;
- 递归版:代码简洁、符合分治思维,性能与迭代版一致,适合偏好递归风格的场景;
- 空间优化版:复用原链表节点,减少内存分配,适合对内存占用要求较高的场景(需注意原链表数据被修改);
- 关键技巧:
- 核心思想:模拟手工加法,逐位计算+进位传递,利用链表逆序存储的特性匹配加法顺序;
- 边界处理:哑节点简化头节点处理,循环条件覆盖“链表长度不一致”和“最后一位进位”;
- 性能优化:优先选择迭代版,避免递归的栈开销;需复用内存时选择空间优化版。