1. 力扣刷题入门:两数相加问题解析
第一次接触力扣(LeetCode)的新手程序员们,往往会被"两数相加"这道经典题目难住。这道看似简单的链表操作题,实际上包含了指针操作、进位处理、边界条件判断等多个编程基本功考点。作为力扣题库的第二题(编号#2),它经常出现在大厂技术面试的第一轮筛选中。
我在去年辅导应届生准备面试时,发现80%的初学者会在这道题上犯至少三种典型错误:忘记处理最后一位进位、指针移动顺序错误、以及链表节点连接混乱。本文将结合我刷遍力扣600+题的经验,带你用Python和C++两种语言完整实现这个算法,并分享那些官方题解不会告诉你的实战技巧。
2. 问题描述与核心难点
2.1 题目要求详解
给定两个非空的链表,表示两个非负整数。每位数字按照逆序方式存储(即个位在链表头),每个节点存储一位数字。要求将这两个数相加,并以相同形式返回结果链表。
示例: 输入:(2 -> 4 -> 3) + (5 -> 6 -> 4) 输出:7 -> 0 -> 8 解释:342 + 465 = 807
2.2 三个关键考察点
- 链表遍历与指针操作:需要同时遍历两个链表并正确处理指针移动
- 进位处理机制:当某位相加结果≥10时,需要保留个位数并进位1
- 边界条件处理:包括链表长度不等、最后一位产生进位等情况
注意:很多面试官会特别关注你对最后一位进位的处理。根据我的面试官经验,这是区分候选人的重要分水岭。
3. Python实现与逐行解析
3.1 完整代码实现
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 digit = total % 10 current.next = ListNode(digit) current = current.next l1 = l1.next if l1 else None l2 = l2.next if l2 else None return dummy.next3.2 关键技巧说明
- 哑节点(dummy node)的使用:避免处理头节点的特殊情况,这是链表题的通用技巧
- 循环条件设计:
while l1 or l2 or carry确保正确处理最后进位 - 三元运算符简化:
l1.val if l1 else 0优雅处理链表长度不等的情况
我在实际刷题中发现,使用//和%分别获取进位和当前位数字,比用if判断更简洁且不易出错。这个技巧也适用于其他需要进位的题目。
4. C++实现与性能优化
4.1 基础版本实现
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) {} }; ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) { ListNode* dummy = new ListNode(); 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; if (l1) l1 = l1->next; if (l2) l2 = l2->next; } return dummy->next; }4.2 内存管理与优化技巧
- 预先计算链表长度:对于超长链表,可以先遍历获取长度,提前分配内存
- 尾指针优化:记录链表尾节点,避免每次从头遍历
- 智能指针应用:在实际工程中建议使用unique_ptr避免内存泄漏
根据力扣官方数据,这个解法的时间复杂度是O(max(m,n)),空间复杂度也是O(max(m,n)),其中m和n分别是两个链表的长度。
5. 常见错误与调试技巧
5.1 新手易犯的5个错误
- 忘记最后的进位:如输入[9,9,9]和[1]时应输出[0,0,0,1]
- 指针移动顺序错误:应该在赋值后再移动current指针
- 链表连接断裂:在移动l1/l2前没有检查是否为nullptr
- 头节点处理不当:没有使用dummy node导致额外判断
- 进位计算错误:误用
total > 10而不是total >= 10
5.2 调试方法与测试用例
推荐使用以下测试用例验证你的代码:
- 等长无进位:[2,4,3] + [5,6,4] = [7,0,8]
- 不等长有进位:[9,9,9,9] + [9,9] = [8,9,0,0,1]
- 最后产生进位:[5] + [5] = [0,1]
- 含零的特殊情况:[0] + [0] = [0]
- 超长链表测试:构造100+节点的链表
我在实际调试时发现,在循环体内打印current链表和carry值是最有效的调试方法。对于C++版本,可以使用valgrind检查内存泄漏。
6. 面试扩展与变种问题
6.1 高频follow-up问题
- 如果数字是正序存储的如何解决?(即[3,4,2]表示342)
- 如何优化空间复杂度到O(1)?
- 如果链表可能包含环怎么处理?
- 如何用递归实现这个算法?
- 如果数字特别大(超过long long范围)怎么办?
6.2 正序存储的解决方案
对于数字正序存储的情况,通常有两种解法:
- 使用栈反转链表
- 先遍历链表计算长度,然后递归处理
递归解法的核心思路:
def addTwoNumbers(l1, l2): def helper(n1, n2, carry): if not n1 and not n2 and not carry: return None val1 = n1.val if n1 else 0 val2 = n2.val if n2 else 0 total = val1 + val2 + carry node = ListNode(total % 10) node.next = helper(n1.next if n1 else None, n2.next if n2 else None, total // 10) return node return helper(l1, l2, 0)7. 刷题进阶建议
7.1 同类题目推荐
- 力扣#445:两数相加II(数字正序存储)
- 力扣#369:给单链表加一
- 力扣#67:二进制求和(字符串形式的二进制数相加)
- 力扣#43:字符串相乘(大数乘法)
- 力扣#415:字符串相加(大数加法)
7.2 个人刷题心得
- 画图辅助:在纸上画出链表指针的变化过程
- 测试驱动:先写测试用例再写代码
- 计时练习:尝试在15分钟内完成从理解到AC
- 多种解法:至少掌握迭代和递归两种实现
- 错题整理:建立自己的错题本,记录每个错误原因
我在准备Google面试时,这道题反复做了7遍,每次都有新的收获。建议初学者至少练习3遍:第一遍看题解,第二遍自己实现,第三遍尝试优化。