# 两数相加 (Add Two Numbers) LeetCode 2. Medium ## 题目描述 给你两个 **非空** 的链表,表示两个非负的整数。它们每位数字都是按照 **逆序** 的方式存储的,并且每个节点只能存储 **一位** 数字。 请你将两个数相加,并以相同形式返回一个表示和的链表。 你可以假设除了数字 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] ``` ## 思路推导 ### 暴力解法分析 **思路1:先转成整数,相加后再转回链表** ```python def addTwoNumbers(l1, l2): # 1. 链表转整数 num1 = 0 while l1: num1 = num1 * 10 + l1.val l1 = l1.next num2 = 0 while l2: num2 = num2 * 10 + l2.val l2 = l2.next # 2. 整数相加 total = num1 + num2 # 3. 整数转链表(逆序) dummy = ListNode(0) if total == 0: return ListNode(0) while total > 0: digit = total % 10 # 需要头插法保持逆序 new_node = ListNode(digit) new_node.next = dummy.next dummy.next = new_node total //= 10 return dummy.next ``` **时间复杂度**:O(max(m, n)) **空间复杂度**:O(1) **问题分析**: - ❌ **整数溢出**:如果链表很长(如100位),会超出整型范围 - ❌ **不符合题意**:题目就是要我们处理大数相加,不能依赖语言的大整数特性 - ❌ **需要反转**:整数转链表时需要头插法,增加复杂度 ### 优化思考 **观察**: - ✅ 链表已经是逆序存储(个位在表头) - ✅ 每个节点只存储一位数字 - ✅ 就像小学竖式加法,从个位开始加 **关键洞察**: ``` 竖式加法: 3 4 2 (链表: 2 -> 4 -> 3) + 4 6 5 (链表: 5 -> 6 -> 4) --------- 8 0 7 (链表: 7 -> 0 -> 8) 从个位开始: 个位:2 + 5 = 7,无进位 十位:4 + 6 = 10,写0进1 百位:3 + 4 + 1(进位) = 8 ``` ### 为什么这样思考? **1. 逆序存储的巧思** - 如果是正序存储,需要先反转链表 - 逆序存储天然符合加法从低位开始的习惯 - 可以直接从表头开始处理 **2. 同时遍历的合理性** - 两个链表对应位相加 - 类似于合并两个有序链表的思路 - 使用指针同时移动 **3. 进位的处理** - 每次计算都要考虑上一位的进位 - `sum = l1.val + l2.val + carry` - 新进位 = `sum / 10` - 当前位 = `sum % 10` **4. 为什么是 max(m, n)** - 两个链表长度可能不同 - 短链表遍历完后,长链表继续处理 - 还要考虑最后的进位可能增加一位 --- ## 解题思路 ### 核心思想 **模拟竖式加法**:同时遍历两个链表,逐位相加并处理进位,使用哑节点简化头节点处理。 ### 详细算法流程 **步骤1:初始化变量** ```go dummy := &ListNode{0, nil} // 哑节点,简化头节点处理 curr := dummy // 当前节点指针 carry := 0 // 进位,初始为0 ``` **关键点**: - 为什么需要哑节点? - 统一处理逻辑,不需要特殊判断第一个节点 - 简化代码,减少if-else - carry初始为什么是0? - 第一次相加前没有进位 **步骤2:同时遍历两个链表** ```go for l1 != nil || l2 != nil { // 获取当前位的值,如果链表已遍历完则为0 x := 0 if l1 != nil { x = l1.Val l1 = l1.Next } y := 0 if l2 != nil { y = l2.Val l2 = l2.Next } // 计算当前位的和 sum := x + y + carry // 更新进位 carry = sum / 10 // 创建新节点(当前位的值) curr.Next = &ListNode{sum % 10, nil} curr = curr.Next } ``` **关键点**: - 为什么用 `||` 而不是 `&&`? - 只要有一个链表没遍历完就要继续 - 长链表的剩余位需要单独处理 - 为什么 `l1 != nil` 时还要检查? - 避免空指针异常 - 已遍历完的链表对应的位视为0 - 为什么 `sum / 10` 是进位? - 整除,如 12 / 10 = 1(进位) - 为什么 `sum % 10` 是当前位? - 取余,如 12 % 10 = 2(当前位) **步骤3:处理最后的进位** ```go if carry > 0 { curr.Next = &ListNode{carry, nil} } ``` **关键点**: - 为什么需要这个判断? - 例如:9 + 9 = 18,需要创建新节点存储1 - 例如:1 + 2 = 3,不需要创建新节点 - 什么时候会进位? - 最后一位相加 >= 10 **步骤4:返回结果** ```go return dummy.Next ``` **关键点**: - 为什么返回 `dummy.Next` 而不是 `dummy`? - `dummy` 是哑节点,值为0 - `dummy.Next` 才是真正的结果链表头 --- ### 关键细节说明 **细节1:为什么用 `||` 而不是 `&&`?** ```go // ❌ 错误:会漏掉长链表的剩余部分 for l1 != nil && l2 != nil { } // ✅ 正确:处理完所有位 for l1 != nil || l2 != nil { } ``` **示例**: ``` l1: 9 -> 9 -> 9 l2: 9 使用 &&:只处理第一位 9+9=18,漏掉 l1 的剩余两位 使用 ||:处理所有位 9+9=18, 0+9=9, 0+9=9 结果:8 -> 9 -> 9 -> 1 ``` **细节2:为什么需要检查 `l1 != nil`?** ```go // 获取当前位的值 x := 0 if l1 != nil { x = l1.Val l1 = l1.Next } ``` **原因**: - 长度不同的链表,短的遍历完后就为 `nil` - 直接访问 `l1.Val` 会空指针异常 - 遍历完的位视为0,不影响加法结果 **细节3:哑节点的作用** ```go // ❌ 没有哑节点:需要特殊处理第一个节点 var head *ListNode var curr *ListNode if l1 != nil || l2 != nil { head = &ListNode{(l1.Val + l2.Val) % 10, nil} curr = head } // 后续节点处理... // ✅ 有哑节点:统一处理 dummy := &ListNode{0, nil} curr := dummy // 所有节点统一处理... return dummy.Next // 跳过哑节点 ``` **细节4:为什么最后还要判断进位?** ```go if carry > 0 { curr.Next = &ListNode{carry, nil} } ``` **示例**: ``` 情况1:9 + 9 = 18 - 最后 carry = 1 - 需要创建新节点存储1 - 结果:8 -> 1 情况2:1 + 2 = 3 - 最后 carry = 0 - 不需要创建新节点 - 结果:3 ``` --- ### 边界条件分析 **边界1:两个链表长度相同** ``` 输入:l1 = [2,4,3], l2 = [5,6,4] 过程: - 第1位:2+5+0=7, carry=0, 结果:7 - 第2位:4+6+0=10, carry=1, 结果:7->0 - 第3位:3+4+1=8, carry=0, 结果:7->0->8 输出:[7,0,8] ``` **边界2:一个链表很长** ``` 输入:l1 = [9,9,9,9,9,9,9], l2 = [9,9,9,9] 过程: - 第1位:9+9=18, carry=1 - 第2位:9+9+1=19, carry=1 - 第3位:9+9+1=19, carry=1 - 第4位:9+9+1=19, carry=1 - 第5位:9+0+1=10, carry=1 - 第6位:9+0+1=10, carry=1 - 第7位:9+0+1=10, carry=1 - 最后进位:carry=1 输出:[8,9,9,9,0,0,0,1] ``` **边界3:链表只有一个0** ``` 输入:l1 = [0], l2 = [0] 过程: - 第1位:0+0+0=0, carry=0 - 最后进位:carry=0,不需要创建新节点 输出:[0] ``` **边界4:最后有进位** ``` 输入:l1 = [9,9], l2 = [1] 过程: - 第1位:9+1=10, carry=1 - 第2位:9+0+1=10, carry=1 - 最后进位:carry=1,创建新节点 输出:[0,0,1] ``` --- ### Q&A 问题解释 **Q1:为什么链表是逆序存储的?** A:这是设计上的巧思: - ✅ 符合加法从个位开始的习惯 - ✅ 不需要反转链表 - ✅ 可以直接从头指针开始遍历 **Q2:如果链表是正序存储的怎么办?** A:需要反转链表或使用栈: ```go // 方法1:反转链表 func addTwoNumbersReverse(l1, l2 *ListNode) *ListNode { l1 = reverseList(l1) l2 = reverseList(l2) result := addTwoNumbers(l1, l2) return reverseList(result) } // 方法2:使用栈 func addTwoNumbersStack(l1, l2 *ListNode) *ListNode { stack1, stack2 := []*ListNode{}, []*ListNode{} // 入栈 for l1 != nil { stack1 = append(stack1, l1) l1 = l1.Next } for l2 != nil { stack2 = append(stack2, l2) l2 = l2.Next } // 从栈顶(高位)开始相加 carry := 0 var head *ListNode for len(stack1) > 0 || len(stack2) > 0 || carry > 0 { x, y := 0, 0 if len(stack1) > 0 { x = stack1[len(stack1)-1].Val stack1 = stack1[:len(stack1)-1] } if len(stack2) > 0 { y = stack2[len(stack2)-1].Val stack2 = stack2[:len(stack2)-1] } sum := x + y + carry carry = sum / 10 // 头插法(因为是正序) node := &ListNode{sum % 10, head} head = node } return head } ``` **Q3:时间复杂度为什么是 O(max(m, n))?** A: - 两个链表长度分别为 m 和 n - 同时遍历,较短的先遍历完 - 最多遍历 max(m, n) 次 - 最后处理进位是 O(1) - 总时间:O(max(m, n)) **Q4:空间复杂度为什么是 O(1)?** A: - 不考虑结果链表(这是输出必须的) - 只使用了固定数量的变量:dummy, curr, carry, sum, x, y - 不随输入规模增长 - 所以是 O(1) **Q5:如何处理多个链表相加?** A: ```go // 方法1:逐个相加 func addMultipleNumbers(lists []*ListNode) *ListNode { if len(lists) == 0 { return nil } result := lists[0] for i := 1; i < len(lists); i++ { result = addTwoNumbers(result, lists[i]) } return result } // 方法2:分治相加 func addMultipleNumbersDivide(lists []*ListNode) *ListNode { if len(lists) == 0 { return nil } return divide(lists, 0, len(lists)-1) } func divide(lists []*ListNode, left, right int) *ListNode { if left == right { return lists[left] } mid := left + (right-left)/2 l1 := divide(lists, left, mid) l2 := divide(lists, mid+1, right) return addTwoNumbers(l1, l2) } ``` --- ### 复杂度分析(详细版) **时间复杂度**: ``` - 遍历链表:O(max(m, n)) - m 和 n 分别为两个链表的长度 - 同时遍历,短的先结束 - 处理进位:O(1) - 最多一次操作 - 创建新节点:O(max(m, n) + 1) - 最多比长链表多一位 总计:O(max(m, n)) ``` **空间复杂度**: ``` - 不考虑结果链表:O(1) - 只使用固定数量变量 - dummy, curr, carry, sum, x, y - 考虑结果链表:O(max(m, n) + 1) - 结果链表最多比长链表多一位 通常说:O(1)(不考虑结果链表) ``` --- ### 执行过程演示 **示例输入**:l1 = [2,4,3], l2 = [5,6,4] ``` 初始状态: l1: 2 -> 4 -> 3 l2: 5 -> 6 -> 4 dummy -> 0 carry = 0 === 第1次循环 === x = 2, y = 5 sum = 2 + 5 + 0 = 7 carry = 7 / 10 = 0 curr.Next = 7 dummy -> 7 l1: 4 -> 3, l2: 6 -> 4 === 第2次循环 === x = 4, y = 6 sum = 4 + 6 + 0 = 10 carry = 10 / 10 = 1 curr.Next = 0 dummy -> 7 -> 0 l1: 3, l2: 4 === 第3次循环 === x = 3, y = 4 sum = 3 + 4 + 1 = 8 carry = 8 / 10 = 0 curr.Next = 8 dummy -> 7 -> 0 -> 8 l1: nil, l2: nil === 循环结束 === carry = 0,不需要创建新节点 === 返回结果 === return dummy.Next -> [7,0,8] ``` --- ### 常见错误 **错误1:忘记处理最后的进位** ```go // ❌ 错误 for l1 != nil || l2 != nil { // ... 计算逻辑 } return dummy.Next // 忘记检查carry // ✅ 正确 for l1 != nil || l2 != nil { // ... 计算逻辑 } if carry > 0 { // 检查最后的进位 curr.Next = &ListNode{carry, nil} } return dummy.Next ``` **错误2:使用 && 而不是 ||** ```go // ❌ 错误:会漏掉长链表的剩余部分 for l1 != nil && l2 != nil { // ... } // ✅ 正确 for l1 != nil || l2 != nil { // ... } ``` **错误3:忘记检查链表是否为nil** ```go // ❌ 错误:空指针异常 x := l1.Val // l1可能为nil // ✅ 正确 x := 0 if l1 != nil { x = l1.Val } ``` **错误4:返回dummy而不是dummy.Next** ```go // ❌ 错误:多了一个0 return dummy // ✅ 正确 return dummy.Next ``` **错误5:进位计算错误** ```go // ❌ 错误:应该用整除 carry = sum / 10.0 // 浮点除法 // ✅ 正确 carry = sum / 10 // 整除 ``` --- --- ## 进阶问题 ### Q1: 如果链表是正序存储的,如何解决? **方法**:反转链表 + 上述算法,或使用栈 ```go func addTwoNumbersReverse(l1, l2 *ListNode) *ListNode { // 反转链表 l1 = reverseList(l1) l2 = reverseList(l2) // 使用相同算法相加 result := addTwoNumbers(l1, l2) // 反转结果 return reverseList(result) } func reverseList(head *ListNode) *ListNode { var prev *ListNode for head != nil { next := head.Next head.Next = prev prev = head head = next } return prev } ``` ### Q2: 如何处理多个链表相加? **方法**:逐个相加或使用优先队列(最小堆) --- ## P7 加分项 ### 深度理解 - **链表操作**:指针移动、内存管理 - **边界处理**:不同长度链表、最后进位 - **哑节点技巧**:简化代码逻辑 ### 实战扩展 - **大数据场景**:链表表示超大整数相加 - **分布式场景**:MapReduce 实现大数相加 - **面试追问**:时间复杂度能否优化?如何测试? ### 变形题目 1. [445. 两数相加 II](https://leetcode.cn/problems/add-two-numbers-ii/) - 链表正序存储 2. [链表相加系列](https://leetcode.cn/tag/linked-list/) - 更多链表操作 --- ## 总结 这道题的核心是: 1. **模拟加法**:逐位相加,处理进位 2. **链表遍历**:同时遍历两条链表 3. **边界处理**:不同长度、最后进位 **易错点**: - 忘记处理最后的进位 - 链表长度不一致时的处理 - 哑节点的使用(简化代码) **最优解法**:一次遍历,时间 O(max(m,n)),空间 O(1)