题目描述
给你两个非空链表,表示两个非负整数。链表中每个节点存储一位数字,且数字以逆序 方式存储(例如:2-->4-->3
表示整数342)。现在,请将两个数相加,并以相同形式返回表示和的链表。
示例:
ini
输入:l1 = [2,4,3], l2 = [5,6,4]
输出:[7,0,8]
解释:342 + 465 = 807,返回链表 7-->0-->8
解法1:基础迭代法(模拟竖式计算)
核心思想:模拟人工竖式计算的过程,逐位相加并处理进位。
javascript
function addTwoNumbers(l1, l2) {
const dummy = new ListNode(0); // 哑节点简化操作
let curr = dummy;
let carry = 0; // 进位值
while (l1 || l2 || carry) {
// 获取当前位的值(节点不存在则为0)
const val1 = l1 ? l1.val : 0;
const val2 = l2 ? l2.val : 0;
// 计算当前位的和(包括进位)
const sum = val1 + val2 + carry;
carry = Math.floor(sum / 10); // 更新进位
curr.next = new ListNode(sum % 10); // 创建新节点
// 移动指针
curr = curr.next;
if (l1) l1 = l1.next;
if (l2) l2 = l2.next;
}
return dummy.next; // 返回真正的头节点
}
解法说明:
-
初始化 :创建哑节点
dummy
简化链表操作,然后用carry
记录进位值(初始为0),curr
指向当前结果链表的末尾。 -
循环处理 :同时遍历
l1
和l2
,直到两个链表都结束且无进位后,再计算当前位的和(节点值+进位),在更新进位值carry = sum / 10
(取整)后,创建新节点存储sum % 10
的结果。 -
指针移动 :将
curr
移动到新创建的节点,l1
和l2
也移动到各自的下一个节点。 -
返回结果 :循环结束返回
dummy.next
(跳过哑节点)
时间复杂度 :O(max(m,n))
空间复杂度:O(max(m,n))
解法2:递归解法(函数式思维)
核心思想:将加法过程分解为递归步骤,每层递归处理一位数字。
javascript
function addTwoNumbers(l1, l2, carry = 0) {
// 递归终止条件:无节点且无进位
if (!l1 && !l2 && carry === 0) return null;
// 计算当前位的和
const val1 = l1 ? l1.val : 0;
const val2 = l2 ? l2.val : 0;
const sum = val1 + val2 + carry;
// 创建当前节点
const node = new ListNode(sum % 10);
// 递归处理下一位
node.next = addTwoNumbers(
l1 ? l1.next : null,
l2 ? l2.next : null,
Math.floor(sum / 10)
);
return node;
}
解法说明:
- 递归终止条件:先判断两个链表为空,且无进位的情况下,返回null。
- 当前位计算 :获取当前节点值,若存在则将节点的值赋值给
val1
/val2
,否则则为0,随后进行计算(值1+值2+进位)。 - 创建节点 :创建一个节点用于存储
sum % 10
的结果 - 递归连接 :通过递归处理下一位,并将结果连接到
node.next
。 - 返回节点:返回当前创建的节点
时间复杂度 :O(max(m,n))
空间复杂度:O(max(m,n))
解法3:原地修改法(空间优化)
核心思想:复用较长的链表节点,减少新节点的创建。
javascript
function addTwoNumbers(l1, l2) {
let p1 = l1, p2 = l2;
let carry = 0;
let lastNode = null;
while (p1 || p2 || carry) {
// 获取当前值
const val1 = p1 ? p1.val : 0;
const val2 = p2 ? p2.val : 0;
const sum = val1 + val2 + carry;
// 复用p1或p2的节点
if (p1) {
p1.val = sum % 10;
lastNode = p1;
p1 = p1.next;
} else if (p2) {
p2.val = sum % 10;
lastNode = p2;
p2 = p2.next;
} else {
// 处理最后进位
lastNode.next = new ListNode(carry);
carry = 0; // 进位已处理
break;
}
carry = Math.floor(sum / 10);
}
return l1; // 或l2,取决于哪个更长
}
优点:
- 复用已有节点,减少内存分配。
- 处理最后进位时创建新节点。
- 优先复用
l1
的节点,当l1
结束时使用l2
。
总结
1. 方法对比:
解法 | 优势 | 劣势 | 适用场景 |
---|---|---|---|
迭代法 | 逻辑清晰,效率稳定 | 需要额外空间 | 通用场景,面试首选 |
递归法 | 代码简洁,数学思维 | 栈空间开销 | 函数式编程,短链表 |
原地修改法 | 空间效率高 | 修改输入,逻辑复杂 | 内存敏感,允许修改输入 |
2. 逆序存储的优势
链表的逆序存储(个位在头部)带来天然对齐的优势:
- 不需要考虑数字位数对齐问题
- 可以直接从头部开始逐位相加
- 进位自然向链表尾部传播
3. 哑节点技巧
为什么使用哑节点?
graph LR
A[dummy] --> B[节点1]
B --> C[节点2]
C --> D[...]
- 统一操作:避免对头节点的特殊处理
- 简化逻辑 :始终有
curr.next = newNode
- 安全返回 :
dummy.next
直接指向结果头节点