当前:LC2 · 两数相加 · 首次出现于 Day 11 · 路径:顶栏「56天打卡」→ 点击 LC 题号 → 逐题动画

LC2数据结构链表 · 进位模拟链表指针接力

两数相加

2→4→3 表示 342(head 是个位),与 5→6→4 逐位竖式相加得 7→0→8。

题目是什么

2→4→3 表示 342(head 是个位),与 5→6→4 逐位竖式相加得 7→0→8。

解决什么问题

改指针之前需要保存哪个后继,哪些节点已经属于结果链?

核心结论

六阶段:逆序数位 → 竖式对齐 → 逐位 sum/digit/carry → dummy 脚手架 → 99+1 边界 → 代码映射。

01交互算法精讲

先说结论:这道题到底解决什么

怎样从“2→4→3 表示 342(head 是个位),与 5→6→4 逐位竖式相加得 7→0→8。”推导出 链表 · 进位模拟,并证明每次状态变化都不会漏掉答案?

中心结论:六阶段:逆序数位 → 竖式对齐 → 逐位 sum/digit/carry → dummy 脚手架 → 99+1 边界 → 代码映射。

读完必须能回答
  1. 1.暴力方案在哪里重复计算,为什么仍然是正确基线?
  2. 2.改指针之前需要保存哪个后继,哪些节点已经属于结果链?
  3. 3.不变量“每一位的结果只依赖当前位与 carry,与更高位无关。”为什么能保证算法安全前进?
02交互算法精讲

完整题目与题意拆解

2 个逆序的链表,要求从低位开始相加,得出结果也逆序输出,返回值是逆序结果链表的头结点。

在本站主例中,2→4→3 表示 342(head 是个位),与 5→6→4 逐位竖式相加得 7→0→8。

算法最终需要得到或观察:结果链表 7→0→8,表示 807。

把题目翻译成状态
  • 输入:2→4→3 表示 342(head 是个位),与 5→6→4 逐位竖式相加得 7→0→8。
  • 机器需要维护:当前位指针 l1/l2、进位 carry、结果链表尾指针。
  • 最终可观察结果:结果链表 7→0→8,表示 807。
动画 1 · 题意扫描

先看清算法到底要维护什么

先建立输入、目标、输出和第一批状态,不急着进入模板。

Step 1/20%
这不是 243 + 564,而是 342 + 465misconception
正在加载 LC2 进位工厂...
03交互算法精讲

第一层方案:暴力做法

复制整条链表或反复寻找前驱会重复走节点;哨兵和指针重连直接维护局部关系。

暴力方案的价值是确认题意并提供正确性基线。它通常会覆盖所有候选, 但没有保存已经确认的信息,因此同一状态会被重新计算。

动画 2 · 暴力重复

重复工作究竟发生在哪里

把重复读取或重复搜索的区域明确标出,再决定优化必须保存什么。

Step 1/20%
先做对:建立暴力基线枚举所有候选并完整验证
正在加载 LC2 进位工厂...
优化方向:六阶段:逆序数位 → 竖式对齐 → 逐位 sum/digit/carry → dummy 脚手架 → 99+1 边界 → 代码映射。
04交互算法精讲

整体地图:先做什么,再做什么

  1. 1建模把输入翻译成“链表指针接力”,明确答案需要观察什么。
  2. 2状态只维护 当前位指针 l1/l2、进位 carry、结果链表尾指针。
  3. 3转移每一步按照 六阶段:逆序数位 → 竖式对齐 → 逐位 sum/digit/carry → dummy 脚手架 → 99+1 边界 → 代码映射。
  4. 4收尾读取 结果链表 7→0→8,表示 807。,并复核边界与复杂度。
05交互算法精讲

链表指针接力:核心概念

画清每根 next 指针改变前后的归属,再写代码。

这道题不是为了记住一个组件名称,而是为了让状态具有可解释的语义:当前位指针 l1/l2、进位 carry、结果链表尾指针。

核心不变量
  • 每一位的结果只依赖当前位与 carry,与更高位无关。
动画 3 · 核心概念

建立“链表指针接力”心智模型

用主例建立核心状态,先预测下一步,再公开正确分支和理由。

Step 1/40%
这不是 243 + 564,而是 342 + 465misconception
正在加载 LC2 进位工厂...
06交互算法精讲

核心机制:状态如何一步步变化

需要注意的是各种进位问题。

极端情况,例如 ``` Input: (9 -> 9 -> 9 -> 9 -> 9) + (1 -> ) Output: 0 -> 0 -> 0 -> 0 -> 0 -> 1 ```

为了处理方法统一,可以先建立一个虚拟头结点,这个虚拟头结点的 Next 指向真正的 head,这样 head 不需要单独处理,直接 while 循环即可。另外判断循环终止的条件不用是 p.Next != nil,这样最后一位还需要额外计算,循环终止条件应该是 p != nil。

六阶段:逆序数位 → 竖式对齐 → 逐位 sum/digit/carry → dummy 脚手架 → 99+1 边界 → 代码映射。

执行过程中持续维护:当前位指针 l1/l2、进位 carry、结果链表尾指针。

正确性依赖以下不变量:每一位的结果只依赖当前位与 carry,与更高位无关。

面试时可以压缩为:dummy+尾指针,while(l1||l2||carry) 逐位求和写结点;短链补 0,最后 carry 非零再补一位。

落到当前题,执行机制可以压缩为:六阶段:逆序数位 → 竖式对齐 → 逐位 sum/digit/carry → dummy 脚手架 → 99+1 边界 → 代码映射。每一次更新都必须保持核心不变量,而不是只让样例碰巧得到正确答案。

动画 4 · 机制构建

一次状态转移为什么成立

集中观察一次选择、计算和状态写回,让变量变化与原因同时出现。

Step 1/50%
Round 1:2 + 5 + 0 = 7Round 1
正在加载 LC2 进位工厂...
07交互算法精讲

正确性证明:为什么不会漏答案

初始化

初始化:算法开始时,全部合法候选仍在状态表示范围内;每一位的结果只依赖当前位与 carry,与更高位无关。

保持

保持:执行“六阶段:逆序数位 → 竖式对齐 → 逐位 sum/digit/carry → dummy 脚手架 → 99+1 边界 → 代码映射。”时,只删除已经能证明不可能的候选,并把新信息写回 当前位指针 l1/l2、进位 carry、结果链表尾指针。

终止

终止:没有待处理状态或达到命中条件时,当前可观察结果就是“结果链表 7→0→8,表示 807。”。

正确性抓手不是“样例跑通”,而是每一帧结束后仍能复述:每一位的结果只依赖当前位与 carry,与更高位无关。
08交互算法精讲

完整执行过程

  1. 1这不是 243 + 564,而是 342 + 465链表的 head 不是最高位,而是个位。LC2 故意倒着存,是为了让我们从 head 开始直接做竖式加法。 因为:这不是 243 + 564,而是 342 + 465
  2. 2先高亮个位:2 + 5普通竖式从右往左加,链表刚好把最右边的个位放在最前面,所以不用反转链表。 因为:head 在个位,所以不必反转链表
  3. 3Round 3:3 + 4 + 1 = 8百位加上飞来的 carry=1:3+4+1=8。carry 清零,结果链 dummy → 7 → 0 → 8,表示 807。 因为:每轮:取两位 + carry → digit + 新 carry
  4. 4Round 1:9 + 1 + 0 = 10个位 9+1=10:写 0,carry=1。结果 dummy → 0。 因为:99 + 1:链表结束 ≠ 计算结束
  5. 5Round 3:l1/l2 都没了,但 carry=1这就是为什么 while 必须包含 carry != 0。链表结束不代表计算结束,还要补结点 1,得到 0→0→1 表示 100。 因为:99 + 1:链表结束 ≠ 计算结束
  6. 6sum 从 carry 累加每轮先把 carry 放进 sum,再按需加上 l1、l2 的当前位(没有则跳过)。 因为:过程懂了,再对照 Go 实现
  7. 7digit 与 carry 拆分digit = sum % 10 写入新结点;carry = sum / 10 传给下一位。 因为:过程懂了,再对照 Go 实现
  8. 8我现在应该记住什么逆序链表 + 竖式同向扫描 + 逐位进位 + dummy 脚手架 + while 含 carry —— 五句话串起整题。 因为:过程懂了,再对照 Go 实现
动画 5 · 完整执行

从输入完整走到可观察结果

从主例第一帧运行到答案,时间轴始终显示当前状态、因果解释和下一步。

Step 1/80%
这不是 243 + 564,而是 342 + 465misconception
正在加载 LC2 进位工厂...
09交互算法精讲

把动画和 Go 代码逐行对应

代码窗口不会按容易漂移的固定数字行号硬绑动画,而是根据当前语义阶段, 在完整 Go 实现中定位选择、计算、写回或返回分支。当前变量与高亮行一起变化。

动画 6 · 代码映射

让每个动作都落到 Go 分支

重新执行关键分支,只显示当前代码附近窗口,并说明这行为什么在此刻运行。

Step 1/60%
这不是 243 + 564,而是 342 + 465misconception
正在加载 LC2 进位工厂...
10交互算法精讲

完整 Go 提交代码与最小测试

完整 Go 解法
// ListNode define
/**
 * Definition for singly-linked list.
 * type ListNode struct {
 *     Val int
 *     Next *ListNode
 * }
 */

func addTwoNumbers(l1 *ListNode, l *ListNode) *ListNode {
	head := &ListNode{Val: 0}
	n1, n, carry, current := 0, 0, 0, head
	for l1 != nil || l != nil || carry != 0 {
		if l1 == nil {
			n1 = 0
		} else {
			n1 = l1.Val
			l1 = l1.Next
		}
		if l == nil {
			n = 0
		} else {
			n = l.Val
			l = l.Next
		}
		current.Next = &ListNode{Val: (n1 + n + carry) % 10}
		current = current.Next
		carry = (n1 + n + carry) / 10
	}
	return head.Next
}
最小测试集合
func main() {
    // 1. 主例
    //    输入:mode="add-two", values=[2,4,3], valuesB=[5,6,4]
    //    期望:结果链表 7→0→8,表示 807。
    //
    // 2. 失败 / 未命中
    //    检查:忘记最后 carry=1 时还要补一个新结点。
    //
    // 3. 边界
    //    空输入、单元素、最小合法规模,以及答案恰好落在边界的情况。
    //
    // 4. 迁移
    //    LC21 合并两个有序链表;LC445 两数相加 II
}

Go 参考实现基于 halfrost/LeetCode-Go 的 MIT 许可代码整理,并按本站教学结构补充解释与动画映射。

11交互算法精讲

正确性与复杂度

时间复杂度 O(max(m,n))

执行过程中只保留仍可能影响答案的状态。六阶段:逆序数位 → 竖式对齐 → 逐位 sum/digit/carry → dummy 脚手架 → 99+1 边界 → 代码映射。

空间复杂度 O(1)

额外状态主要用于维护:当前位指针 l1/l2、进位 carry、结果链表尾指针。

终局不变量
  • 每一位的结果只依赖当前位与 carry,与更高位无关。
12交互算法精讲

最容易写错的地方

错误 1

忘记最后 carry=1 时还要补一个新结点。

边界复查

必须额外检查空输入、单元素、未命中或不可达情况,以及恰好落在边界的输入。

13交互算法精讲

最后复盘:带走逻辑链

  1. 1题意2→4→3 表示 342(head 是个位),与 5→6→4 逐位竖式相加得 7→0→8。
  2. 2重复复制整条链表或反复寻找前驱会重复走节点;哨兵和指针重连直接维护局部关系。
  3. 3优化六阶段:逆序数位 → 竖式对齐 → 逐位 sum/digit/carry → dummy 脚手架 → 99+1 边界 → 代码映射。
  4. 4证明每一位的结果只依赖当前位与 carry,与更高位无关。
  5. 5复杂度时间 O(max(m,n)),空间 O(1)
面试表达:dummy+尾指针,while(l1||l2||carry) 逐位求和写结点;短链补 0,最后 carry 非零再补一位。
迁移练习
  • LC21 合并两个有序链表
  • LC445 两数相加 II