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]

codex分析

AC代码

/**
 * 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) {}
 * };
 */
class Solution {
public:
    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
        ListNode dummy;
        ListNode* l3 = &dummy;//取地址符
        int book=0;
        while(l1!=nullptr||l2!=nullptr||book!=0){
            int sum=book?1:0;
            if(l1!=nullptr){
                sum+=l1->val;
                l1=l1->next;
            }
            if(l2!=nullptr){
                sum+=l2->val;
                 l2=l2->next;
            }
            l3->next = new ListNode(sum % 10);
            l3 = l3->next;
            book = sum / 10;
        }
        return dummy.next;
    }
};

问题1:为什么要定义一个dummy指针

dummy 不是必须的,它是一个哨兵节点,用来让链表构造更方便。
在这题里,我们要不断创建新节点:
cur->next = new ListNode(sum % 10);
cur = cur->next;
问题是:第一个节点比较特殊。因为一开始结果链表还不存在,如果不用 dummy,你就得单独判断:

if (head == nullptr) {
    head = new ListNode(sum % 10);
    cur = head;
} else {
    cur->next = new ListNode(sum % 10);
    cur = cur->next;
}

用了 dummy 之后,就不用特殊处理第一个节点:

ListNode dummy;
ListNode* cur = &dummy;

然后每次都统一接到 cur->next 后面:

cur->next = new ListNode(sum % 10);
cur = cur->next;

最后返回:
return dummy.next;
因为 dummy 本身只是临时占位,真正的结果链表是从 dummy.next 开始的。
可以这样理解:

dummy -> 2 -> 4 -> 3
         ^
       真正答案从这里开始

所以 dummy 的作用就是:省掉“第一个节点怎么处理”的麻烦,让后续所有节点都用同一套逻辑添加。

问题2:ListNode* l3 = &dummy;的写法是什么意思 & 符号的意义

& 的核心意义就是:取变量的地址,让指针能指向它。

问题3:不用dummy,此时的代码实现,返回是l3吗

可以不用 dummy,但这时返回的一般是头指针 l3,同时还需要另一个指针负责往后接节点。
比如:

class Solution {
public:
    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
        ListNode* l3 = nullptr;   // 结果链表的头
        ListNode* cur = nullptr;  // 结果链表的尾部当前位置
        int carry = 0;

        while (l1 != nullptr || l2 != nullptr || carry != 0) {
            int sum = carry;

            if (l1 != nullptr) {
                sum += l1->val;
                l1 = l1->next;
            }

            if (l2 != nullptr) {
                sum += l2->val;
                l2 = l2->next;
            }

            ListNode* node = new ListNode(sum % 10);

            if (l3 == nullptr) {
                l3 = node;
                cur = node;
            } else {
                cur->next = node;
                cur = cur->next;
            }

            carry = sum / 10;
        }

        return l3;
    }
};

这里:
ListNode* l3 = nullptr;
表示结果链表一开始为空。
第一次创建节点时:

l3 = node;
cur = node;

意思是:
l3 指向结果链表第一个节点
cur 也指向当前最后一个节点
之后再创建节点,就接到 cur->next:

cur->next = node;
cur = cur->next;

所以最后返回:
return l3;
因为 l3 一直保存着结果链表的头节点。
不过要注意:你不能一直移动 l3。比如这样就有问题:
l3 = l3->next;
如果你把 l3 往后移动了,最后它就不再指向头节点,返回会错。
所以不用 dummy 时通常要两个指针:
l3 // 保存头节点,用来 return
cur // 移动,用来接新节点
一句话:不用 dummy 时,返回 l3;但 l3 必须始终指向结果链表的第一个节点。

问题4:cur->next = node;cur = cur->next; 这种写法怎么理解,先定义下个指向,再赋值

总结:非赋值逻辑,而是移到下一个指针
对,你可以理解成两步:

cur->next = node;
cur = cur->next;
···
假设当前结果链表是:
```java
l3 -> 2 -> 4
           ^
          cur

现在新建了一个节点:
node -> 3
第一句:
cur->next = node;
意思是:让当前尾节点 4 的 next 指向新节点 3。
链表变成:

l3 -> 2 -> 4 -> 3
           ^
          cur

注意,这时候 cur 还指向 4。
第二句:
cur = cur->next;
意思是:把 cur 往后移动一格,让它指向刚刚接上的新节点。
变成:

l3 -> 2 -> 4 -> 3
                ^
               cur

所以这两句的逻辑是:
先把新节点接到链表尾部
再把 cur 移动到新的尾部
也可以写成:

cur->next = node;
cur = node;

因为前面刚刚执行了:
cur->next = node;
所以 cur->next 和 node 指向的是同一个新节点。
不过这两句不能反过来写:

cur = cur->next;
cur->next = node;

因为原来的 cur->next 可能是 nullptr,这样 cur 会先变空,再访问 cur->next 就会出错。

题目理解(CSDN解析)

链表表示法

题目中的链表表示方式比较特殊:

  • 数字的每一位存储在链表的节点中
  • 逆序存储:个位在链表头部,最高位在链表尾部
  • 每个节点存储一位数字(0-9)

例如:数字 342 表示为链表 2 -> 4 -> 3

问题本质

这道题的本质是模拟 竖式加法

  1. 从最低位(链表头部)开始相加
  2. 处理进位
  3. 逐位计算,直到两个链表都遍历完且没有进位为止

解法一:迭代法(推荐)

算法思路

  1. 初始化一个哑节点(dummy node)作为结果链表的头节点前驱
  2. 初始化进位 carry = 0
  3. 同时遍历两个链表:
    • 计算当前位的和:sum = val1 + val2 + carry
    • 当前位的结果:sum % 10
    • 新的进位:carry = sum // 10
    • 创建新节点连接到结果链表
  4. 如果遍历完后还有进位,需要额外创建一个节点
  5. 返回哑节点的下一个节点

时间复杂度

  • 时间复杂度:O(max(m, n)),其中 m 和 n 分别是两个链表的长度
  • 空间复杂度:O(max(m, n)),结果链表的长度最多为 max(m, n) + 1

Python 实现

# Definition for singly-linked list.
class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next

class Solution:
    def addTwoNumbers(self, l1: Optional[ListNode], l2: Optional[ListNode]) -> Optional[ListNode]:
        dummy = ListNode(0)  # 哑节点
        current = dummy
        carry = 0
        
        while l1 or l2 or carry:
            # 获取当前节点的值,如果节点为空则为0
            val1 = l1.val if l1 else 0
            val2 = l2.val if l2 else 0
            
            # 计算当前位的和
            total = val1 + val2 + carry
            carry = total // 10  # 计算进位
            digit = total % 10   # 计算当前位的数字
            
            # 创建新节点
            current.next = ListNode(digit)
            current = current.next
            
            # 移动指针
            if l1:
                l1 = l1.next
            if l2:
                l2 = l2.next
        
        return dummy.next

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 dummy = new ListNode(0);
        ListNode current = dummy;
        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 dummy.next;
    }
}

C++ 实现

/**
 * 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) {}
 * };
 */
class Solution {
public:
    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
        ListNode* dummy = new ListNode(0);
        ListNode* current = dummy;
        int carry = 0;
        
        while (l1 != nullptr || l2 != nullptr || carry != 0) {
            int val1 = (l1 != nullptr) ? l1->val : 0;
            int val2 = (l2 != nullptr) ? 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 != nullptr) l1 = l1->next;
            if (l2 != nullptr) l2 = l2->next;
        }
        
        ListNode* result = dummy->next;
        delete dummy;  // 防止内存泄漏
        return result;
    }
};

解法二:递归法

算法思路

  1. 递归函数接收两个链表节点和进位作为参数
  2. 递归终止条件:两个链表都为空且进位为0
  3. 递归计算当前位的和,创建节点
  4. 递归计算下一位

Python 递归实现

class Solution:
    def addTwoNumbers(self, l1: Optional[ListNode], l2: Optional[ListNode]) -> Optional[ListNode]:
        def add(l1, l2, carry):
            # 递归终止条件
            if not l1 and not l2 and carry == 0:
                return None
            
            # 计算当前位的和
            val1 = l1.val if l1 else 0
            val2 = l2.val if l2 else 0
            total = val1 + val2 + carry
            
            # 创建当前节点
            node = ListNode(total % 10)
            
            # 递归计算下一个节点
            next1 = l1.next if l1 else None
            next2 = l2.next if l2 else None
            node.next = add(next1, next2, total // 10)
            
            return node
        
        return add(l1, l2, 0)

算法流程图

开始:l1, l2

初始化 dummy 节点和进位 carry=0

l1 或 l2 或 carry 不为空?

计算当前位和 sum = val1 + val2 + carry

计算进位 carry = sum // 10

创建新节点 digit = sum % 10

连接到结果链表

移动 l1, l2 指针

返回 dummy.next

关键点与注意事项

1. 哑节点(Dummy Node)技巧

  • 使用哑节点可以简化链表操作,避免处理头节点的特殊情况
  • 最后返回 dummy.next 即可

2. 进位处理

  • 进位的初始值为0
  • 每次计算后更新进位:carry = sum // 10
  • 循环结束后如果还有进位,需要额外创建节点

3. 链表长度不一致

  • 当一个链表遍历完而另一个未遍历完时,将已遍历完的链表视为0
  • 使用条件判断:val1 = l1.val if l1 else 0

4. 循环条件

  • 循环条件应为 while l1 or l2 or carry,而不是 while l1 and l2
  • 这样可以处理链表长度不一致和最后有进位的情况

测试用例设计

基础测试

# 测试用例1:正常情况
l1 = ListNode(2, ListNode(4, ListNode(3)))  # 342
l2 = ListNode(5, ListNode(6, ListNode(4)))  # 465
# 预期结果:7 -> 0 -> 8 (807)

# 测试用例2:有进位
l1 = ListNode(9, ListNode(9))  # 99
l2 = ListNode(1)  # 1
# 预期结果:0 -> 0 -> 1 (100)

# 测试用例3:长度不一致
l1 = ListNode(1)  # 1
l2 = ListNode(9, ListNode(9))  # 99
# 预期结果:0 -> 0 -> 1 (100)

边界测试

  1. 两个链表都只有一个节点
  2. 一个链表为空(题目保证非空,但可以测试)
  3. 结果有多个进位:999 + 1 = 1000
  4. 大数相加:链表很长的情况

复杂度分析

时间复杂度

  • 需要遍历两个链表的所有节点
  • 时间复杂度为 O(max(m, n)),其中 m 和 n 分别是两个链表的长度

空间复杂度

  • 除了结果链表外,只使用了常数级别的额外空间
  • 结果链表的长度最多为 max(m, n) + 1
  • 空间复杂度为 O(max(m, n))

常见错误

错误1:忘记处理最后的进位

# 错误代码
while l1 or l2:
    # ... 计算
# 如果最后还有进位,会丢失

错误2:循环条件错误

# 错误代码
while l1 and l2:  # 当一个链表遍历完就停止
    # ...

错误3:指针移动错误

# 错误代码
l1 = l1.next  # 如果 l1 可能为 None,会报错
# 应该先判断
if l1:
    l1 = l1.next

扩展思考

1. 如果链表是正序存储的怎么办?

如果链表是正序存储(最高位在头部),可以:

  1. 先反转链表,然后使用本题解法
  2. 使用栈来反转顺序
  3. 递归计算

2. 如何优化空间复杂度?

  • 可以在较长的链表上直接修改,而不是创建新链表
  • 但需要注意链表不可变的情况

3. 多个链表相加

  • 可以扩展为多个链表相加
  • 使用类似的思路,同时遍历多个链表

总结

两数相加是链表操作中的经典题目,考察了:

  1. 链表的基本操作
  2. 数学运算中的进位处理
  3. 边界条件的处理能力

掌握这道题的关键在于:

  • 理解逆序存储的特点
  • 熟练使用哑节点简化操作
  • 正确处理进位和链表长度不一致的情况

建议初学者先掌握迭代法,再尝试递归法,最后思考各种边界情况和扩展问题。

相关题目推荐

  1. LeetCode 445. 两数相加 II(链表正序存储)
  2. LeetCode 21. 合并两个有序链表
  3. LeetCode 206. 反转链表
  4. LeetCode 369. 给单链表加一

参考资料

  1. LeetCode 官方题解
  2. 链表基础知识
  3. 算法可视化:两数相加

最后更新:2024年1月
标签:链表、数学、模拟、算法
难度:中等
通过率:约 42.3%

更多推荐