{ id : 'top-progress-bar', // 请勿修改该值 color : '#77b6ff', height : '2px', duration: 0.2, }

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:进位值,初始为 0
  • sum:当前位的和 = l1.val + l2.val + carry
  • 新节点值sum % 10
  • 新进位sum / 10

处理要点

  1. 同时遍历两个链表
  2. 较短链表遍历完后,用 0 补位
  3. 遍历结束后,若 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. 算法原理描述

核心思想:复用较长的链表存储结果,减少新节点的创建。

实现方式

  1. 确定较长的链表作为结果链表
  2. 在原链表上直接修改节点值
  3. 仅在需要时(最后的进位)创建新节点

⚠️ 注意:此方法会修改原链表,某些场景下可能不适用。

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: 如果数字是正序存储怎么办?

:两种方法:

  1. 反转链表:先反转两个链表,用本题方法相加,再反转结果
  2. 使用栈:将两个链表的值分别入栈,然后出栈相加

十一、一图总结

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;
}
posted @ 2025-12-19 14:40  星空Dreamer  阅读(4)  评论(0)    收藏  举报