D

当前:LC133 · 克隆图 · 首次出现于 Day 31 · 路径:顶栏「56天打卡」→ 点击 LC 题号 → 逐题动画

LC133

克隆图 · 为什么 old → clone 映射表才是本题核心

DFS · HashMap 防环

给定图中任意一个节点,返回这张无向连通图的深拷贝。 新图节点必须是全新对象,但结构、节点值与邻居关系都要与旧图一致。 难点在于图有环、有共享邻居,所以必须维护一张cloneMap: old → clone

当前样例
正方形 · 4 节点
邻接: 1↔2, 1↔4, 2↔3, 3↔4 — 经典 LC133 样例,含环
图存在多个环 (1→2→3→4→1) 与共享邻居 — 必须依赖 map 才能正确停下
V = 4, E = 4, entry = old 1
输出
深拷贝新图入口
返回 clone 1,结构与 old 1 同构
时间复杂度O(V + E)
每个节点恰被 dfs 创建 1 次,每条边恰被 append 1 次
空间复杂度O(V)
cloneMap 存 V 个键值;递归栈最深 O(V)

这题不是复制一个节点,而是复制一整张关系网

题目只给你图里的一个入口节点 node。 但这个 node 不是孤立的,它通过 neighbors 连接着其他节点; 其他节点又继续连接更多节点。所以这题真正要做的是:

  1. 从入口 node 出发,沿着 neighbors 把整张旧图走一遍;
  2. 为旧图里的每个节点创建一个全新的 clone 节点;
  3. 再把 clone 节点之间的邻居关系也一模一样地连起来;
  4. 最后返回新图里的入口节点 cloneNode
旧图 old graph
只读 · 题目给你的输入
1234
cloneMap
old → clone 的对照表
old 1clone 1
old 2clone 2
old 3clone 3
old 4clone 4
新图 cloned graph
全新对象 · 你要返回的输出
c1c2c3c4
旧图不能改新图必须是全新对象结构必须一模一样

三个概念先讲清楚

完全没接触过图也没关系,先把这三个词弄懂,后面就顺了。

什么是连通?

连通的意思是:从任意一个节点出发,只要沿着 neighbors 一直走, 最终可以走到图里的所有节点

比如从节点 1 出发,可以走到 2 和 4;从 2 又可以走到 3;所以 1、2、3、4 都能被访问到。 这就是为什么题目只给你一个 node,你仍然能克隆整张图。

补充提示 · 如果图不连通,只给你一个入口节点,你是无法知道其他孤岛节点存在的。

什么是深拷贝?

深拷贝不是把旧节点直接拿来用。旧图里的节点 1,和新图里的节点 1', 值可以一样、邻居关系也可以一样,但它们必须是两个不同的对象

也就是说:old1 != clone1。

错误
clone1.Neighbors = old1.Neighbors
浅拷贝,新图仍然连着旧节点
正确
clone1.Neighbors = append(clone1.Neighbors, clone2)
新图只连接新节点

什么是 neighbors?

每个节点都有一个 Neighbors 列表, 表示它直接连接的节点。

如果 1.neighbors = [2, 4], 说明节点 1 直接连接节点 2 和节点 4。

补充提示 · 因为这是无向图,所以 2 的 neighbors 里通常也会有 1。

怎么把这道题建模成代码?

A

Step A · 节点 Node

type Node struct {
    Val       int
    Neighbors []*Node
}

Val 是节点编号, Neighbors 是它直接连接的邻居节点列表。

B

Step B · 边 Edge

如果 1 的 neighbors 里有 2,就表示 1 和 2 之间有一条

无向图里,这条边可以从 1 走到 2,也可以从 2 走回 1。

old 1 old 2
C

Step C · 入口 node

题目没有给你完整数组,也没有给你所有节点列表。它只给你一个入口节点 node

你需要从这个入口开始,通过 DFS BFS 沿着 neighbors 探索,边走边发现整张图。

D

Step D · 状态 cloneMap

克隆图时,我们需要同时记住两件事:

  1. 旧节点有没有被克隆过?
  2. 如果克隆过,它对应的新节点是谁?

所以需要 cloneMap[oldNode] = clonedNode:

old 1clone 1'
old 2clone 2'
old 3clone 3'

整个算法只围绕 4 个动作

查 map

进入一个旧节点 cur 后,先看它是否已经克隆过。如果 cloneMap[cur] 存在,直接返回已有 clone,避免重复克隆和无限递归。

建 clone

如果 cur 没有被克隆过,创建一个新节点 clone,值等于 cur.Val。

递归邻居

遍历 cur.Neighbors,对每个邻居调用 dfs(neighbor),先把邻居那一侧的 clone 拿到手。

连接 clone 边

把 dfs(neighbor) 返回的 clone append 到 clone.Neighbors,新图这条边才算连好。

关键顺序 · 立刻写 map:创建 clone 后必须 立刻 执行 cloneMap[cur] = clone, 这一步必须发生在递归邻居之前 —— 否则遇到环时会无限递归。

为什么必须有 cloneMap?

图与树不同: 图可能有环,也可能多个节点共享同一个邻居。 如果你只用“裸 DFS + 不停 new Node”,会立刻出两个 bug:

Bug ① 遇到环 → 无限递归

old 1 → old 2 → old 1 → old 2 … 没有 map 就无法判断“我已经在克隆这个节点了”, 调用栈瞬间爆掉。

Bug ② 同一节点被复制多次

如果 old 2 和 old 4 都把 old 1 当邻居,各自创建一个新 clone 1, 新图里就出现了两个不同的 1 —— 结构错了。

解药 · old → clone 映射表

每次进入一个 old 节点,先查 cloneMap:已存在 → 直接复用,不重建也不再递归; 不存在 → 创建 clone 并立刻写入 map,然后再递归邻居。 这条规则同时解决“环”和“共享邻居”。

测试样例 · 切换后整段动画会重算

题目Step 1/31

旧图 old graph

只读 · 不会被修改
1234
当前 dfs 节点正被检视的邻居其他

cloneMap · old → clone

0 条记录
cloneMap = ∅
还没有 dfs 进入过任何节点

新图 cloned graph

0 个 clone · 0 条边
new graph = ∅
等待第一次 visit-create 帧
刚创建 / 当前命中复用 (绿色)刚追加的边
当前发生了什么

题目目标 · 复制整张图,不是复制一个点

题目只给你图里的一个入口节点 node。但 node 不是孤立的,它通过 neighbors 连着别的节点,别的节点又连着更多节点。所以真正要做的是:从入口 old 1 出发,沿 neighbors 把整张旧图走一遍,为每个旧节点造一个全新的 clone,再把 clone 之间的邻居关系也一模一样地连起来,最后返回新图入口 clone 1。

cloneMap 现状
∅ (还没登记任何节点)
新图本步新增
本步暂无新增节点 / 边

为什么这一步是对的

深拷贝意味着不能把旧引用塞进新结构,所有 *Node 必须是新分配的。

面试该怎么说 · 先复述需求:深拷贝整张图,新对象、相同结构。再点明图可能有环 → 需要 map 记忆已克隆的节点。

DFS 调用栈

stack 为空 (尚未进入或已全部返回)

代码同步 · Go

题目
1func cloneGraph(node *Node) *Node {
2 if node == nil {
3 return nil
4 }
5
6 cloneMap := map[*Node]*Node{}
7
8 var dfs func(*Node) *Node
9 dfs = func(cur *Node) *Node {
10 if cloned, ok := cloneMap[cur]; ok {
11 return cloned
12 }
13
14 clone := &Node{Val: cur.Val}
15 cloneMap[cur] = clone
16
17 for _, nei := range cur.Neighbors {
18 clone.Neighbors = append(clone.Neighbors, dfs(nei))
19 }
20
21 return clone
22 }
23
24 return dfs(node)
25}
行 14cloneMap[cur] = clone · 写入这一行是阻断环的关键 — 后续任何路径再遇到 cur 都会直接复用。
行 17clone.Neighbors = append(...) · 这一行复制边。光复制点不复制邻居,结构就缺一半。

剧本时间线 · 共 31

正确性不变量

在任意时刻, 只要 old 节点出现在 cloneMap 中,它就已经拥有唯一对应的 clone 节点。 之后任何地方再遇到这个 old 节点,都必须复用这个 clone。

这条不变量同时给出了正确性(无重复节点、邻居关系准确)与复杂度上界 (每个节点构造 1 次 · 每条边 append 1 次)。

错误做法 vs 正确做法

错误做法

  • ✗ 每次遇到邻居就 new Node — 没有 map,同一旧节点被反复 new 出多个新对象,新图里出现多个同值不同实例的“假冒节点”。
  • ✗ 没有先写入 map 就递归邻居 — 在 cloneMap[cur] = clone 之前调用 dfs(nei),环路上的递归会一路杀回 cur, 而 cur 还没进 map → 无限递归。
  • ✗ 只复制节点,忘记复制 neighbors — clone 出来了,但 clone.Neighbors 是空切片。 新图只是一堆孤立的节点,没有边。

正确做法

  1. 先查 map:if cloned, ok := cloneMap[cur]; ok 就立刻 return。
  2. 不存在 → 创建 clone 并立刻写入 map:cloneMap[cur] = clone。这一步必须在递归之前。
  3. 再递归复制邻居并连边:clone.Neighbors = append(clone.Neighbors, dfs(nei))。 dfs(nei) 的返回值就是 clone 邻居 —— 它要么是新创建的,要么是 map 命中复用的。

面试 · 一段话讲清楚

这题是图的深拷贝。 难点在于图可能有环,所以不能直接递归 new 节点。 我用一个哈希表记录 old node → cloned node 的映射。 DFS 时先判断当前节点是否已经克隆过,如果克隆过直接返回; 否则创建新节点放入 map,然后递归克隆所有邻居,并把克隆后的邻居加入当前 clone 的 neighbors。 每个点和每条边只处理一次,所以时间 O(V+E),空间 O(V)

常见误区

  • ✗ 空间复杂度写成 O(1) — 忽略了 cloneMap 与递归栈,正确是 O(V)
  • ✗ 时间写成 O(n) 而不说 n 是什么 — 准确表述是 O(V + E)
  • ✗ 忘记空图保护 — node == nil 要直接返回 nil。
  • ✗ 用值作 map key 是可以的(本题节点值唯一),但更通用的是用 *Node 指针作 key。