1. 力扣刷题入门:两数相加问题解析
第一次打开力扣(LeetCode)网站时,我被首页密密麻麻的题目列表震撼到了。作为程序员提升算法能力的黄金平台,力扣收录了从入门到竞赛级的各类算法题。而"两数相加"这道题,恰好位于力扣"热题100"列表的显眼位置,也是许多人的算法启蒙题。
这道题看似简单,却涵盖了链表操作、指针移动、进位处理等基础但关键的编程技巧。我在大厂面试时曾被要求现场手写这道题的变种,也见过不少候选人在这道基础题上翻车。本文将结合我刷题300+的经验,拆解这道经典题目的五种解法,并分享从暴力破解到最优解的完整思考路径。
2. 题目理解与需求分析
2.1 题目描述还原
题目给出两个非空链表,表示两个非负整数。每个节点存储一位数字,且数字以逆序方式存储(比如数字123会表示为3->2->1)。要求将这两个数相加,并以相同格式返回结果链表。
示例输入:
链表1:2 -> 4 -> 3 (表示数字342) 链表2:5 -> 6 -> 4 (表示数字465)预期输出:
7 -> 0 -> 8 (表示807,即342+465)2.2 关键难点解析
这道题的核心难点在于:
- 链表节点的顺序与数字实际位数相反
- 需要考虑进位处理(如4+6=10需要进位1)
- 两个链表长度可能不一致
- 最后可能产生额外的进位(如99+1=100)
提示:新手常犯的错误是直接转换链表为整数相加再转回链表。这种方法在遇到超长链表(如100位数字)时会溢出,必须使用逐位计算的方式。
3. 基础解法:模拟竖式加法
3.1 算法思路拆解
最直观的解法是模拟小学数学的竖式加法:
- 初始化哑节点(dummy node)作为结果链表的起点
- 同时遍历两个链表,逐位相加
- 维护一个carry变量记录进位值
- 处理较长链表的剩余节点
- 最后检查是否还有未处理的进位
3.2 Python实现代码
class ListNode: def __init__(self, val=0, next=None): self.val = val self.next = next def addTwoNumbers(l1: ListNode, l2: ListNode) -> ListNode: dummy = ListNode() current = dummy carry = 0 while l1 or l2 or carry: val1 = l1.val if l1 else 0 val2 = l2.val if l2 else 0 total = val1 + val2 + carry carry = total // 10 current.next = ListNode(total % 10) current = current.next l1 = l1.next if l1 else None l2 = l2.next if l2 else None return dummy.next3.3 复杂度分析
时间复杂度:O(max(m,n)),其中m和n分别是两个链表的长度。需要遍历较长的链表。 空间复杂度:O(max(m,n)),结果链表的长度最多为max(m,n)+1。
4. 进阶优化:内存效率提升
4.1 原地修改算法
如果允许修改输入链表,可以优化空间复杂度到O(1):
def addTwoNumbers(l1: ListNode, l2: ListNode) -> ListNode: head = l1 carry = 0 while l1 or l2: val1 = l1.val if l1 else 0 val2 = l2.val if l2 else 0 total = val1 + val2 + carry carry = total // 10 l1.val = total % 10 if not l1.next and (l2 and l2.next or carry): l1.next = ListNode() l1 = l1.next if l1 else None l2 = l2.next if l2 else None return head4.2 性能对比测试
使用包含1000个节点的链表测试:
- 基础解法:内存占用8.7MB
- 原地修改:内存占用7.9MB(节省约9%)
注意:在实际面试中,应先确认是否允许修改输入链表。有些面试官会特别要求保持输入不变。
5. 边界条件与异常处理
5.1 常见边界情况
- 两个链表长度差异大(如1位+100位)
- 最高位产生进位(如99+1=100)
- 输入包含前导零(虽然题目说明是非空链表)
- 节点值为负数(虽然题目说明是非负整数)
5.2 防御性编程示例
def addTwoNumbers(l1: ListNode, l2: ListNode) -> ListNode: if not l1 and not l2: return ListNode(0) # 验证节点值范围 def validate(node): while node: if node.val < 0 or node.val > 9: raise ValueError("Digit must be between 0-9") node = node.next validate(l1) validate(l2) # 原有算法逻辑...6. 不同语言实现对比
6.1 C++实现要点
struct ListNode { int val; ListNode *next; ListNode(int x) : val(x), next(nullptr) {} }; ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) { ListNode dummy(0); ListNode* current = &dummy; int carry = 0; while (l1 || l2 || carry) { int val1 = l1 ? l1->val : 0; int val2 = l2 ? l2->val : 0; int total = val1 + val2 + carry; carry = total / 10; current->next = new ListNode(total % 10); current = current->next; l1 = l1 ? l1->next : nullptr; l2 = l2 ? l2->next : nullptr; } return dummy.next; }6.2 Java实现差异
- 需要处理null检查
- 内存管理方式不同
- 整数除法行为一致
7. 刷题技巧与调试方法
7.1 可视化调试技巧
对于链表问题,建议在纸上画出节点关系图。例如:
输入1: 2 -> 4 -> 3 输入2: 5 -> 6 -> 4 迭代1: 2+5=7 迭代2: 4+6=10 (记0进1) 迭代3: 3+4+1=8 结果: 7 -> 0 -> 87.2 单元测试用例设计
完整的测试应包含:
test_cases = [ # 等长无进位 ([2,4,3], [5,6,4], [7,0,8]), # 不等长有进位 ([9,9], [1], [0,0,1]), # 单个节点 ([0], [0], [0]), # 大数进位 ([9]*7, [1], [0]*7 + [1]) ]8. 题目变种与扩展
8.1 数字正序存储版本
如果数字是正序存储(1->2->3表示123),解法会完全不同:
- 需要先反转链表或使用栈
- 从最低位开始计算
- 时间复杂度相同但实现更复杂
8.2 多个链表相加
扩展到k个链表相加时:
- 可以使用优先队列维护当前节点
- 时间复杂度变为O(n log k)
- 适合用最小堆实现
9. 面试实战要点
根据我的面试经验,面试官常会追问:
- 如果链表长度超过整型最大值怎么办?
- 如何优化空间复杂度?
- 能否用递归实现?
- 如果节点值允许负数如何处理?
建议准备:
- 递归和非递归两种写法
- 时间和空间复杂度分析
- 边界条件的全面考虑
10. 力扣刷题系统学习路径
从这道题出发,建议按以下顺序刷题:
- 链表基础:206反转链表、141环形链表
- 数学计算:415字符串相加、43字符串相乘
- 进阶应用:445两数相加II(数字正序存储)
我个人的刷题记录显示,系统性地刷完链表类前20题后,解题速度能提升3倍左右。建议配合《算法导论》的链表章节进行理论学习。