LeetCode 2. 两数相加 (Add Two Numbers)
一、题目描述
原题
给你两个非空的链表,表示两个非负的整数。它们每位数字都是按照逆序的方式存储的,并且每个节点只能存储一位数字。
请你将两个数相加,并以相同形式返回一个表示和的链表。
你可以假设除了数字 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]
解释:9999999 + 9999 = 10009998
图示说明
示例1图解:
l1: 2 -> 4 -> 3 表示数字 342
l2: 5 -> 6 -> 4 表示数字 465
---------------------------
结果: 7 -> 0 -> 8 表示数字 807
约束条件
- 每个链表中的节点数在范围
[1, 100]内 0 <= Node.val <= 9- 题目数据保证列表表示的数字不含前导零
链表节点定义
// Definition for singly-linked list.
struct ListNode {
int val;
ListNode *next;
ListNode() : val(0), next(nullptr) {}
ListNode(int x) : val(x), next(nullptr) {}
ListNode(int x, ListNode *next) : val(x), next(next) {}
};
二、题目解析
核心问题拆解
| 关键点 | 分析 |
|---|---|
| 输入 | 两个非空链表(逆序存储数字) |
| 输出 | 一个新链表(逆序存储和) |
| 逆序存储 | 链表头是个位,便于从低位开始相加 |
| 核心难点 | 进位处理、链表长度不等、最后的进位 |
为什么用逆序存储?
flowchart LR
subgraph 逆序["逆序存储 ✅ 本题采用"]
A1["2"] --> A2["4"] --> A3["3"]
B1["个位"] --> B2["十位"] --> B3["百位"]
end
subgraph 优势["优势"]
C1["从链表头开始"]
C2["自然对应加法顺序"]
C3["进位向后传递"]
end
逆序 --> 优势
思考方向
flowchart TD
A["两数相加"] --> B["模拟竖式加法"]
B --> C["从个位开始逐位相加"]
C --> D["处理进位 carry"]
D --> E{"需要考虑的情况"}
E --> F["两链表等长"]
E --> G["两链表不等长"]
E --> H["最后仍有进位"]
三、算法解答
算法一:模拟法(迭代)⭐ 标准解法
1. 算法原理描述
核心思想:模拟手工竖式加法,从个位(链表头)开始,逐位相加并处理进位。
关键变量:
carry:进位值,初始为 0sum:当前位的和 =l1.val + l2.val + carry新节点值:sum % 10新进位:sum / 10
处理要点:
- 同时遍历两个链表
- 较短链表遍历完后,用 0 补位
- 遍历结束后,若
carry > 0,需额外创建节点
2. 算法解答过程
以 l1 = [2,4,3], l2 = [5,6,4] 为例:
| 步骤 | l1.val | l2.val | carry(入) | sum | 新节点值 | carry(出) |
|---|---|---|---|---|---|---|
| 1 | 2 | 5 | 0 | 7 | 7 | 0 |
| 2 | 4 | 6 | 0 | 10 | 0 | 1 |
| 3 | 3 | 4 | 1 | 8 | 8 | 0 |
结果链表:7 -> 0 -> 8,表示 807
3. 算法原理图像解析
flowchart TD
subgraph 输入["输入链表"]
direction LR
L1["l1: 2 → 4 → 3 (表示342)"]
L2["l2: 5 → 6 → 4 (表示465)"]
end
subgraph Step1["步骤1: 个位相加"]
S1A["2 + 5 + 0 = 7"]
S1B["节点值: 7 % 10 = 7"]
S1C["进位: 7 / 10 = 0"]
S1A --> S1B --> S1C
end
subgraph Step2["步骤2: 十位相加"]
S2A["4 + 6 + 0 = 10"]
S2B["节点值: 10 % 10 = 0"]
S2C["进位: 10 / 10 = 1"]
S2A --> S2B --> S2C
end
subgraph Step3["步骤3: 百位相加"]
S3A["3 + 4 + 1 = 8"]
S3B["节点值: 8 % 10 = 8"]
S3C["进位: 8 / 10 = 0"]
S3A --> S3B --> S3C
end
subgraph 结果["结果链表"]
R["7 → 0 → 8 (表示807)"]
end
输入 --> Step1 --> Step2 --> Step3 --> 结果
进位传递过程:
flowchart LR
subgraph 竖式["模拟竖式加法"]
direction TB
A[" 3 4 2"]
B["+ 4 6 5"]
C["-------"]
D[" 8 0 7"]
end
subgraph 链表["链表表示"]
direction LR
L1["2→4→3"]
L2["5→6→4"]
L3["7→0→8"]
end
竖式 --> 链表
算法流程图:
flowchart TD
Start["开始"] --> Init["初始化: carry = 0<br/>创建哑节点 dummy"]
Init --> Loop{"l1 或 l2 不为空<br/>或 carry > 0?"}
Loop -->|"是"| GetVal["获取当前值<br/>x = l1 ? l1.val : 0<br/>y = l2 ? l2.val : 0"]
GetVal --> CalcSum["计算 sum = x + y + carry"]
CalcSum --> NewNode["创建新节点<br/>val = sum % 10"]
NewNode --> UpdateCarry["更新 carry = sum / 10"]
UpdateCarry --> MovePtr["移动指针<br/>l1 = l1.next<br/>l2 = l2.next"]
MovePtr --> Loop
Loop -->|"否"| Return["返回 dummy.next"]
4. 算法代码
class Solution {
public:
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
// 创建哑节点,简化头节点处理
ListNode* dummy = new ListNode(0);
ListNode* curr = dummy;
int carry = 0; // 进位
// 遍历两个链表,直到都为空且无进位
while (l1 != nullptr || l2 != nullptr || carry != 0) {
// 获取当前位的值,为空则用0
int x = (l1 != nullptr) ? l1->val : 0;
int y = (l2 != nullptr) ? l2->val : 0;
// 计算当前位的和
int sum = x + y + carry;
// 创建新节点
curr->next = new ListNode(sum % 10);
curr = curr->next;
// 更新进位
carry = sum / 10;
// 移动指针
if (l1 != nullptr) l1 = l1->next;
if (l2 != nullptr) l2 = l2->next;
}
return dummy->next;
}
};
5. 代码详解
// 关键点解析:
// 1. 为什么使用哑节点 (dummy node)?
// - 避免单独处理头节点的特殊情况
// - 统一所有节点的插入逻辑
// - 最后返回 dummy->next 即为真正的头节点
// 2. 循环条件为什么是 l1 || l2 || carry?
// - l1 || l2:确保较长链表的剩余部分被处理
// - carry:确保最后的进位被处理
// 例如:[9,9] + [1] = [0,0,1],最后的1来自进位
// 3. 为什么用三元运算符取值?
// int x = (l1 != nullptr) ? l1->val : 0;
// - 当链表已遍历完时,用0补位
// - 简化代码,避免分支判断
// 4. sum % 10 和 sum / 10 的作用
// - sum % 10:取个位数,作为新节点的值
// - sum / 10:取进位(0或1)
6. 复杂度分析
| 复杂度类型 | 值 | 说明 |
|---|---|---|
| 时间复杂度 | O(max(m, n)) | m, n 为两链表长度,遍历较长的链表 |
| 空间复杂度 | O(max(m, n)) | 新链表长度最多为 max(m,n) + 1 |
7. 边界情况处理
flowchart TD
subgraph Case1["情况1: 长度不等"]
C1A["l1: 9 → 9"]
C1B["l2: 1"]
C1C["结果: 0 → 0 → 1"]
end
subgraph Case2["情况2: 最后有进位"]
C2A["l1: 9 → 9 → 9"]
C2B["l2: 1"]
C2C["结果: 0 → 0 → 0 → 1"]
end
subgraph Case3["情况3: 全是0"]
C3A["l1: 0"]
C3B["l2: 0"]
C3C["结果: 0"]
end
8. 使用范围
| 场景 | 适用性 |
|---|---|
| 标准两数相加 | ✅ 最优解 |
| 链表长度不等 | ✅ 自动处理 |
| 有进位情况 | ✅ 自动处理 |
| 大数加法 | ✅ 适用(链表不受位数限制) |
算法二:递归法
1. 算法原理描述
核心思想:将问题分解为「当前位相加」+「剩余部分递归处理」。
递归定义:
- 当前操作:计算当前位的和,创建新节点
- 递归调用:处理下一位(l1.next, l2.next, 新进位)
- 终止条件:两链表都为空且无进位
2. 算法解答过程
flowchart TD
subgraph 递归过程["递归调用栈"]
R1["add(2,5,0)"] --> R2["add(4,6,0)"]
R2 --> R3["add(3,4,1)"]
R3 --> R4["add(null,null,0)"]
R4 --> R5["返回 null"]
end
subgraph 回溯过程["回溯构建链表"]
B1["节点8 → null"]
B2["节点0 → 节点8"]
B3["节点7 → 节点0"]
end
递归过程 --> 回溯过程
3. 算法原理图像解析
flowchart TD
subgraph 递归树["递归调用树"]
A["addTwo(2→4→3, 5→6→4, 0)"]
A --> B["创建节点7"]
B --> C["addTwo(4→3, 6→4, 0)"]
C --> D["创建节点0"]
D --> E["addTwo(3, 4, 1)"]
E --> F["创建节点8"]
F --> G["addTwo(null, null, 0)"]
G --> H["返回nullptr"]
end
subgraph 结果["构建结果"]
R1["7"] --> R2["0"] --> R3["8"] --> R4["null"]
end
4. 算法代码
class Solution {
public:
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
return addWithCarry(l1, l2, 0);
}
private:
ListNode* addWithCarry(ListNode* l1, ListNode* l2, int carry) {
// 终止条件:两链表都为空且无进位
if (l1 == nullptr && l2 == nullptr && carry == 0) {
return nullptr;
}
// 获取当前位的值
int x = (l1 != nullptr) ? l1->val : 0;
int y = (l2 != nullptr) ? l2->val : 0;
int sum = x + y + carry;
// 创建当前节点
ListNode* node = new ListNode(sum % 10);
// 递归处理下一位
ListNode* next1 = (l1 != nullptr) ? l1->next : nullptr;
ListNode* next2 = (l2 != nullptr) ? l2->next : nullptr;
node->next = addWithCarry(next1, next2, sum / 10);
return node;
}
};
5. 复杂度分析
| 复杂度类型 | 值 | 说明 |
|---|---|---|
| 时间复杂度 | O(max(m, n)) | 递归深度为较长链表的长度 |
| 空间复杂度 | O(max(m, n)) | 递归调用栈 + 新链表空间 |
6. 使用范围
| 场景 | 适用性 |
|---|---|
| 代码简洁性要求高 | ✅ 适用 |
| 链表较长 | ⚠️ 可能栈溢出 |
| 理解递归思想 | ✅ 教学用途好 |
算法三:原地修改(空间优化)
1. 算法原理描述
核心思想:复用较长的链表存储结果,减少新节点的创建。
实现方式:
- 确定较长的链表作为结果链表
- 在原链表上直接修改节点值
- 仅在需要时(最后的进位)创建新节点
⚠️ 注意:此方法会修改原链表,某些场景下可能不适用。
2. 算法代码
class Solution {
public:
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
ListNode* head = l1; // 使用l1作为结果链表
ListNode* prev = nullptr;
int carry = 0;
while (l1 != nullptr || l2 != nullptr) {
int x = (l1 != nullptr) ? l1->val : 0;
int y = (l2 != nullptr) ? l2->val : 0;
int sum = x + y + carry;
if (l1 != nullptr) {
l1->val = sum % 10; // 直接修改l1的值
prev = l1;
l1 = l1->next;
} else {
// l1已空,需要连接l2的节点
prev->next = l2;
l2->val = sum % 10;
prev = l2;
l2 = l2->next;
l1 = nullptr;
}
carry = sum / 10;
if (l2 != nullptr) l2 = l2->next;
}
// 处理最后的进位
if (carry > 0) {
prev->next = new ListNode(carry);
}
return head;
}
};
3. 复杂度分析
| 复杂度类型 | 值 | 说明 |
|---|---|---|
| 时间复杂度 | O(max(m, n)) | 遍历较长链表 |
| 空间复杂度 | O(1) | 仅在最后进位时创建一个新节点 |
4. 使用范围
| 场景 | 适用性 |
|---|---|
| 允许修改原链表 | ✅ 空间最优 |
| 需要保留原链表 | ❌ 不适用 |
| 对空间要求极高 | ✅ 适用 |
四、三种算法对比
flowchart LR
subgraph 迭代法["迭代法 ✅推荐"]
I1["⏱ O max m,n"]
I2["💾 O max m,n"]
I3["⭐⭐⭐⭐⭐"]
end
subgraph 递归法["递归法"]
R1["⏱ O max m,n"]
R2["💾 O max m,n"]
R3["⭐⭐⭐"]
end
subgraph 原地法["原地修改"]
O1["⏱ O max m,n"]
O2["💾 O 1"]
O3["⭐⭐⭐"]
end
| 算法 | 时间复杂度 | 空间复杂度 | 特点 | 推荐指数 |
|---|---|---|---|---|
| 迭代法 | O(max(m,n)) | O(max(m,n)) | 清晰易懂,标准解法 | ⭐⭐⭐⭐⭐ |
| 递归法 | O(max(m,n)) | O(max(m,n)) | 代码简洁,有栈溢出风险 | ⭐⭐⭐ |
| 原地修改 | O(max(m,n)) | O(1) | 空间最优,但修改原链表 | ⭐⭐⭐ |
五、模拟法三种实现对比分析 🔥
模拟法(迭代法)有三种常见的实现方式,它们的核心逻辑相同,但在头节点处理和内存管理上有所不同。
三种实现代码
实现一:无哑节点(手动处理头节点)
class Solution {
public:
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
ListNode* head = nullptr, *tail = nullptr;
int carry = 0;
while (l1 || l2) {
int a = l1 ? l1->val : 0;
int b = l2 ? l2->val : 0;
int sum = a + b + carry;
if (!head) {
head = tail = new ListNode(sum % 10); // 特判头节点
} else {
tail->next = new ListNode(sum % 10);
tail = tail->next;
}
carry = sum / 10;
if (l1) l1 = l1->next;
if (l2) l2 = l2->next;
}
if (carry > 0) {
tail->next = new ListNode(carry); // 单独处理最后进位
}
return head;
}
};
实现二:哑节点(堆上分配)
class Solution {
public:
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
auto dummy = new ListNode(-1), cur = dummy; // 堆上分配
int t = 0;
while (l1 || l2 || t) {
if (l1) t += l1->val, l1 = l1->next;
if (l2) t += l2->val, l2 = l2->next;
cur = cur->next = new ListNode(t % 10);
t /= 10;
}
return dummy->next; // ⚠️ dummy 未释放,存在内存泄漏
}
};
实现三:哑节点(栈上分配)✅ 推荐
class Solution {
public:
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
ListNode dummy(0); // 栈上分配,自动释放
ListNode* cur = &dummy;
int carry = 0;
while (l1 || l2 || carry) {
int sum = carry;
if (l1) { sum += l1->val; l1 = l1->next; }
if (l2) { sum += l2->val; l2 = l2->next; }
cur->next = new ListNode(sum % 10);
cur = cur->next;
carry = sum / 10;
}
return dummy.next; // 注意是 . 不是 ->
}
};
三种实现核心差异对比
flowchart TD
subgraph 实现一["实现一: 无哑节点"]
A1["手动特判头节点"]
A2["if !head 分支"]
A3["单独处理最后进位"]
A1 --> A2 --> A3
end
subgraph 实现二["实现二: 哑节点-堆分配"]
B1["new ListNode 创建哑节点"]
B2["统一插入逻辑"]
B3["循环包含 carry 检查"]
B4["⚠️ 内存泄漏"]
B1 --> B2 --> B3 --> B4
end
subgraph 实现三["实现三: 哑节点-栈分配 ✅"]
C1["栈上创建哑节点"]
C2["统一插入逻辑"]
C3["循环包含 carry 检查"]
C4["自动释放无泄漏"]
C1 --> C2 --> C3 --> C4
end
详细差异表格
| 对比维度 | 实现一(无哑节点) | 实现二(哑节点-堆) | 实现三(哑节点-栈)✅ |
|---|---|---|---|
| 头节点处理 | if(!head) 特判 |
统一逻辑 | 统一逻辑 |
| 进位处理 | 循环外单独处理 | 循环条件包含 | 循环条件包含 |
| 循环条件 | l1 || l2 |
l1 || l2 || t |
l1 || l2 || carry |
| 内存分配 | n 次 new | n+1 次 new | n 次 new |
| 内存泄漏 | ❌ 无 | ⚠️ 有(dummy未释放) | ❌ 无 |
| 代码简洁度 | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 安全性 | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
差异原因深度分析
1️⃣ 头节点处理方式不同
flowchart LR
subgraph 无哑节点["无哑节点方式"]
N1["head 初始为 nullptr"]
N2{"第一次插入?"}
N3["head = tail = new Node"]
N4["tail->next = new Node"]
N1 --> N2
N2 -->|"是"| N3
N2 -->|"否"| N4
end
subgraph 有哑节点["有哑节点方式"]
D1["dummy 作为虚拟头"]
D2["cur->next = new Node"]
D3["返回 dummy.next"]
D1 --> D2 --> D3
end
无哑节点:需要区分「第一次插入」和「后续插入」两种情况
有哑节点:所有插入操作统一,dummy.next 始终指向真正的头节点
2️⃣ 进位处理方式不同
flowchart TD
subgraph 方式一["实现一: 循环外处理"]
W1["while l1 或 l2"]
W2["循环结束"]
W3{"carry > 0?"}
W4["额外创建节点"]
W1 --> W2 --> W3 -->|"是"| W4
end
subgraph 方式二["实现二/三: 循环内处理"]
L1["while l1 或 l2 或 carry"]
L2["carry 作为循环条件之一"]
L3["自动处理最后进位"]
L1 --> L2 --> L3
end
实现一:循环条件只检查链表,最后进位需要单独 if 处理
实现二/三:循环条件包含 carry,最后进位自然在循环内处理
3️⃣ 内存分配方式不同
flowchart TD
subgraph 堆分配["new ListNode(堆)"]
H1["调用 operator new"]
H2["分配堆内存"]
H3["调用构造函数"]
H4["需手动 delete"]
H1 --> H2 --> H3 --> H4
end
subgraph 栈分配["ListNode dummy(栈)"]
S1["编译期确定大小"]
S2["栈指针移动分配"]
S3["调用构造函数"]
S4["函数结束自动释放"]
S1 --> S2 --> S3 --> S4
end
堆分配 -->|"开销大"| 性能
栈分配 -->|"开销小"| 性能
| 分配方式 | 速度 | 是否需要手动释放 | 适用场景 |
|---|---|---|---|
堆分配 new |
较慢 | ✅ 需要 delete | 生命周期不确定 |
| 栈分配 | 极快 | ❌ 自动释放 | 局部临时变量 |
4️⃣ 为什么实现一可能更快?
flowchart TD
subgraph 原因["性能差异原因分析"]
R1["1. 少一次 new 操作"]
R2["2. CPU 分支预测优化"]
R3["3. LeetCode 环境波动"]
end
R1 --> R1D["哑节点版本多分配一个节点<br/>new 涉及系统调用"]
R2 --> R2D["if !head 只首次为 true<br/>后续预测命中率接近 100%"]
R3 --> R3D["同代码多次提交<br/>时间可能相差 50%+"]
重要提示:LeetCode 运行时间波动很大,不要过度关注几毫秒的差异!
flowchart LR
subgraph 实测现象["LeetCode 时间波动实测"]
S1["第1次提交: 12ms<br/>击败 81%"]
S2["第2次提交: 24ms<br/>击败 17%"]
S3["第3次提交: 8ms<br/>击败 95%"]
end
S1 --- 同一份代码
S2 --- 同一份代码
S3 --- 同一份代码
选择建议
flowchart TD
Q{"你的场景是?"}
Q -->|"面试/日常练习"| A["推荐: 实现三<br/>栈上哑节点"]
Q -->|"追求极致性能"| B["可选: 实现一<br/>无哑节点"]
Q -->|"快速写代码"| C["可选: 实现二<br/>但注意内存泄漏"]
A --> A1["✅ 代码简洁"]
A --> A2["✅ 无内存泄漏"]
A --> A3["✅ 不易出错"]
B --> B1["✅ 少一次分配"]
B --> B2["⚠️ 代码稍复杂"]
C --> C1["✅ 最简洁"]
C --> C2["❌ 有内存泄漏"]
总结:三种实现的本质区别
| 本质区别 | 无哑节点 | 有哑节点 |
|---|---|---|
| 设计思想 | 直接操作真实链表 | 引入虚拟头简化边界 |
| 代码风格 | 需要特判边界情况 | 统一处理所有情况 |
| 内存模型 | 只分配必要节点 | 多一个辅助节点 |
| 适用人群 | 追求极致性能 | 追求代码简洁 |
六、知识点总结
1. 涉及的数据结构
flowchart TD
subgraph 链表["单链表 Linked List"]
A["节点 Node"]
A --> B["val: 存储数据"]
A --> C["next: 指向下一节点"]
end
subgraph 操作["关键操作"]
D["遍历: O n"]
E["插入尾部: O 1 有尾指针"]
F["创建节点: new ListNode"]
end
2. 核心技巧
mindmap
root((两数相加<br/>核心技巧))
哑节点技巧
避免头节点特判
统一插入逻辑
进位处理
carry = sum / 10
val = sum % 10
补零技巧
短链表用0补位
三元运算符简化
循环条件
l1 或 l2 非空
或 carry 非零
3. 代码技巧详解
技巧1:哑节点(Dummy Node)
// 不使用哑节点(需要特判头节点)
ListNode* head = nullptr;
ListNode* curr = nullptr;
// ... 需要判断 head 是否为空
// 使用哑节点(统一处理)
ListNode* dummy = new ListNode(0);
ListNode* curr = dummy;
// ... 直接 curr->next = new ListNode(val)
return dummy->next;
技巧2:三元运算符取值
// 繁琐写法
int x, y;
if (l1 != nullptr) x = l1->val; else x = 0;
if (l2 != nullptr) y = l2->val; else y = 0;
// 简洁写法
int x = l1 ? l1->val : 0;
int y = l2 ? l2->val : 0;
技巧3:循环条件包含 carry
// 错误:遗漏最后的进位
while (l1 || l2) { ... }
// 例如 [5] + [5] = [0,1],最后的1会丢失
// 正确:包含进位检查
while (l1 || l2 || carry) { ... }
4. C++ 链表操作要点
// 1. 创建新节点
ListNode* node = new ListNode(val);
ListNode* node = new ListNode(val, next);
// 2. 遍历链表
while (curr != nullptr) {
// 处理 curr
curr = curr->next;
}
// 3. 尾部插入
tail->next = new ListNode(val);
tail = tail->next;
// 4. 空指针检查
if (node != nullptr) {
// 安全访问
}
七、做题模板
「链表加法」类问题通用模板
/**
* 链表加法通用模板
* 适用于:两个链表表示的数字相加
*/
class Solution {
public:
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
// Step 1: 创建哑节点
ListNode* dummy = new ListNode(0);
ListNode* curr = dummy;
int carry = 0;
// Step 2: 遍历两个链表
while (l1 != nullptr || l2 != nullptr || carry != 0) {
// Step 3: 获取当前位的值(空则为0)
int x = l1 ? l1->val : 0;
int y = l2 ? l2->val : 0;
// Step 4: 计算和与进位
int sum = x + y + carry;
carry = sum / 10;
// Step 5: 创建新节点
curr->next = new ListNode(sum % 10);
curr = curr->next;
// Step 6: 移动指针
if (l1) l1 = l1->next;
if (l2) l2 = l2->next;
}
// Step 7: 返回结果
return dummy->next;
}
};
模板流程图:
flowchart TD
A["开始"] --> B["创建哑节点 dummy<br/>curr = dummy<br/>carry = 0"]
B --> C{"l1 或 l2 非空<br/>或 carry > 0?"}
C -->|"是"| D["x = l1 ? l1.val : 0<br/>y = l2 ? l2.val : 0"]
D --> E["sum = x + y + carry"]
E --> F["创建节点 sum % 10<br/>carry = sum / 10"]
F --> G["移动 curr, l1, l2"]
G --> C
C -->|"否"| H["返回 dummy.next"]
模板变体
变体1:链表减法
// 核心修改:借位处理
int diff = x - y - borrow;
if (diff < 0) {
diff += 10;
borrow = 1;
} else {
borrow = 0;
}
变体2:正序存储的链表相加(LeetCode 445)
// 思路:先反转链表,相加后再反转
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
l1 = reverse(l1);
l2 = reverse(l2);
ListNode* sum = addReversed(l1, l2); // 使用标准模板
return reverse(sum);
}
变体3:多个链表相加
ListNode* addMultiple(vector<ListNode*>& lists) {
ListNode* result = lists[0];
for (int i = 1; i < lists.size(); i++) {
result = addTwoNumbers(result, lists[i]);
}
return result;
}
八、常见错误与陷阱
1. 忘记处理最后的进位
flowchart TD
subgraph 错误["❌ 错误结果"]
E1["输入: 9 + 1"]
E2["输出: 0"]
E3["丢失了进位1"]
E1 --> E2 --> E3
end
subgraph 正确["✅ 正确结果"]
C1["输入: 9 + 1"]
C2["输出: 0 → 1"]
C3["包含最后的进位"]
C1 --> C2 --> C3
end
错误代码 vs 正确代码:
// ❌ 错误代码:漏掉了 carry
while (l1 || l2) { ... }
// ✅ 正确代码:包含 carry 检查
while (l1 || l2 || carry) { ... }
2. 空指针访问
// 错误:可能访问空指针
int x = l1->val; // 如果 l1 为空会崩溃
// 正确:先检查再访问
int x = l1 ? l1->val : 0;
3. 指针移动错误
// 错误:无条件移动
l1 = l1->next; // 如果 l1 已为空会崩溃
// 正确:条件移动
if (l1) l1 = l1->next;
九、相关题目推荐
flowchart TD
A["2. 两数相加"] --> B["445. 两数相加 II<br/>正序存储"]
A --> C["369. 给单链表加一"]
A --> D["66. 加一<br/>数组版本"]
A --> E["415. 字符串相加"]
A --> F["67. 二进制求和"]
| 题号 | 题目 | 难度 | 关联知识点 |
|---|---|---|---|
| 445 | 两数相加 II | 中等 | 链表反转 / 栈 |
| 369 | 给单链表加一 | 中等 | 链表、进位 |
| 66 | 加一 | 简单 | 数组、进位 |
| 415 | 字符串相加 | 简单 | 大数加法 |
| 67 | 二进制求和 | 简单 | 二进制、进位 |
十、面试高频问答
Q1: 为什么链表要逆序存储?
答:逆序存储使得链表头对应数字的个位,便于从低位开始相加,进位自然向后(高位)传递,与手工竖式加法顺序一致。如果正序存储,需要先知道链表长度或使用栈/反转。
Q2: 如何处理两个链表长度不同?
答:使用三元运算符,当某个链表遍历完后,将其值视为 0 继续计算:
int x = l1 ? l1->val : 0;
Q3: 为什么使用哑节点?
答:哑节点(dummy node)简化了头节点的处理:
- 不需要单独判断头节点是否为空
- 统一了所有节点的插入逻辑
- 最后返回
dummy->next即可
Q4: 时间复杂度能否优化到 O(min(m,n))?
答:不能。因为较长的链表需要完全遍历,且可能存在最后的进位,所以时间复杂度下界是 O(max(m,n))。
Q5: 如果数字是正序存储怎么办?
答:两种方法:
- 反转链表:先反转两个链表,用本题方法相加,再反转结果
- 使用栈:将两个链表的值分别入栈,然后出栈相加
十一、一图总结
flowchart TB
subgraph 题目["📝 题目: 两数相加"]
Q["两个逆序链表表示的数<br/>求和返回新链表"]
end
subgraph 思路["💡 核心思路"]
T["模拟竖式加法<br/>逐位相加处理进位"]
end
subgraph 要点["🔑 关键要点"]
K1["哑节点简化头节点处理"]
K2["carry处理进位"]
K3["短链表用0补位"]
K4["循环包含carry检查"]
end
subgraph 复杂度["⚡ 复杂度"]
C1["时间: O max m,n"]
C2["空间: O max m,n"]
end
subgraph 记忆["📌 记忆口诀"]
M["链表加法逐位算<br/>哑节点来打头阵<br/>三元取值补零位<br/>循环别忘进位在"]
end
题目 --> 思路 --> 要点 --> 复杂度 --> 记忆
十二、完整测试代码
#include <iostream>
using namespace std;
// 链表节点定义
struct ListNode {
int val;
ListNode* next;
ListNode() : val(0), next(nullptr) {}
ListNode(int x) : val(x), next(nullptr) {}
ListNode(int x, ListNode* next) : val(x), next(next) {}
};
class Solution {
public:
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
ListNode* dummy = new ListNode(0);
ListNode* curr = dummy;
int carry = 0;
while (l1 || l2 || carry) {
int x = l1 ? l1->val : 0;
int y = l2 ? l2->val : 0;
int sum = x + y + carry;
curr->next = new ListNode(sum % 10);
curr = curr->next;
carry = sum / 10;
if (l1) l1 = l1->next;
if (l2) l2 = l2->next;
}
return dummy->next;
}
};
// 辅助函数:创建链表
ListNode* createList(vector<int>& nums) {
ListNode* dummy = new ListNode(0);
ListNode* curr = dummy;
for (int num : nums) {
curr->next = new ListNode(num);
curr = curr->next;
}
return dummy->next;
}
// 辅助函数:打印链表
void printList(ListNode* head) {
while (head) {
cout << head->val;
if (head->next) cout << " -> ";
head = head->next;
}
cout << endl;
}
int main() {
Solution sol;
// 测试用例1: [2,4,3] + [5,6,4] = [7,0,8]
vector<int> v1 = {2, 4, 3};
vector<int> v2 = {5, 6, 4};
ListNode* l1 = createList(v1);
ListNode* l2 = createList(v2);
ListNode* result = sol.addTwoNumbers(l1, l2);
cout << "Test 1: ";
printList(result); // 输出: 7 -> 0 -> 8
return 0;
}

浙公网安备 33010602011771号