← 返回题目列表

克隆图为什么要用哈希表?DFS 和 BFS 如何实现深拷贝?

高频 中等 第 2 / 30 题 更新于 2026/07/30
DFSBFS深拷贝哈希表

简化版

克隆图要做深拷贝:每个原节点都要对应一个新节点,边关系也要复制。因为图可能有环,必须用哈希表记录“原节点 -> 克隆节点”,既防止重复创建,也防止 DFS/BFS 陷入死循环。

详细版

核心是维护映射 Map<Node, Node> visited。如果当前原节点没克隆过,就先创建它的克隆节点放入 map;然后遍历原节点的邻居,把每个邻居的克隆节点加入当前克隆节点的邻居列表。邻居如果没克隆过,就递归 DFS 或放入 BFS 队列继续处理。

DFS 更自然:函数返回当前节点的克隆体,遇到已经克隆过的节点直接返回 map 里的对象。BFS 更显式:从起点入队,逐层遍历原图,遇到新邻居就创建克隆节点并入队。两者复杂度都是 O(V + E),空间复杂度 O(V)

完整版教学

一、克隆图难在哪里

如果图是一棵树,复制节点时从根往下复制即可,因为每个节点只有一条父路径。但图可能有环,也可能有多个节点指向同一个邻居。如果不记录“哪些原节点已经克隆过”,同一个节点会被复制多次,环还会导致无限递归。

例如三角形图:

1 --- 2
 \   /
   3

从 1 克隆到 2,再从 2 克隆到 3,再从 3 回到 1,如果没有 map,程序会再次克隆 1,然后继续绕圈。哈希表的作用就是给每个原节点建立唯一身份证:原节点只创建一次克隆节点,之后所有边都指向这一个克隆对象。

二、哈希表保存的不是访问状态,而是对象映射

普通 DFS 的 visited 只需要记录某个节点是否访问过;克隆图的 map 更强,它记录的是 oldNode -> newNode。这使我们既能判断是否访问过,也能拿到已经创建好的克隆对象。

普通遍历 visited克隆图 map
只判断是否访问过判断是否访问过,并返回克隆节点
值通常是 boolean值是新创建的 Node
防止重复走防止重复创建和死循环

假设原图有 4 个节点、5 条无向边。最终 map 中应该有 4 条记录,而克隆图邻接表里会复制出 10 个邻居引用,因为无向边在两个端点的 neighbors 中各出现一次。

三、DFS 递归实现

DFS 写法的关键是“先创建当前节点,再递归处理邻居”。先放入 map 很重要,因为后续邻居可能马上通过环指回当前节点;如果等邻居都处理完再放 map,就挡不住环。

class Solution {
    private Map<Node, Node> map = new HashMap<>();

    public Node cloneGraph(Node node) {
        if (node == null) return null;
        if (map.containsKey(node)) return map.get(node);

        Node copy = new Node(node.val);
        map.put(node, copy);
        for (Node nei : node.neighbors) {
            copy.neighbors.add(cloneGraph(nei));
        }
        return copy;
    }
}

这个函数的返回值是“当前原节点对应的克隆节点”。递归边界不是叶子节点,而是“这个节点已经在 map 里”。图没有叶子概念,环图里尤其不能按树的思路找终点。

四、BFS 迭代实现

BFS 的思路是从起点开始,把原节点放进队列。每弹出一个原节点,就遍历它的所有邻居;如果邻居没有克隆过,立刻创建克隆节点并入队;然后把邻居的克隆节点连到当前克隆节点上。

创建 copy(start),start 入队
while queue not empty:
  cur = poll()
  for nei in cur.neighbors:
    if nei not in map:
      map[nei] = new Node(nei.val)
      queue.offer(nei)
    map[cur].neighbors.add(map[nei])

BFS 的优势是不会吃递归栈,适合节点数量很大或语言递归限制较低的场景。DFS 更短,BFS 更稳定;面试时任选一种写清楚即可。

五、深拷贝要复制节点和边

深拷贝不是只复制节点值,也不是复用原 neighbors。新图的每个节点都应该是新对象,且新对象之间的连接关系与原图一致。判断是否是深拷贝,可以用两个条件:对象地址不同,结构关系相同。

原图: A -> [B, C]
克隆: A' -> [B', C']

正确: A' != A, B' != B, C' != C
错误: A' 的 neighbors 里直接放 B 或 C

如果复用原节点,修改克隆图会影响原图;这违反了深拷贝语义。这个追问经常用来区分“会写遍历”和“理解对象引用”的候选人。

六、复杂度怎么分析

每个节点最多被创建一次,每条邻接关系最多被扫描一次,所以时间复杂度是 O(V + E)。对于无向图,如果输入邻接表中每条边出现两次,扫描的是邻接表长度,仍然写成 O(V + E),其中 E 可以按逻辑边或邻接项口径说明清楚。

空间复杂度主要来自 map,保存 V 个映射;DFS 还有递归栈,最坏 O(V);BFS 有队列,最坏也是 O(V)。如果图为空,返回 null,不要创建多余节点。

七、常见误区与追问

记忆钩子:克隆图先建“原到新”的字典,再按原图边关系给新节点连边;没有这张字典,环和共享邻居都会出问题。

  • 误区:用节点值当哈希表 key。 节点值不一定能代表对象唯一性,应该用原节点对象本身作为 key。
  • 误区:先递归完邻居再把当前节点放入 map。 有环时会在邻居回指当前节点时无限递归,必须先放 map。
  • 误区:克隆节点后直接复用原 neighbors。 这只是浅拷贝,新图仍然指向旧节点,修改会互相影响。
  • 追问:DFS 和 BFS 哪个更好? 两者复杂度相同,DFS 简洁,BFS 避免递归栈风险。
  • 追问:如果图不连通怎么办? 题目若只给一个起点,克隆起点可达的连通分量;如果要求克隆整张图,需要遍历所有节点作为入口。
  • 追问:为什么复杂度不是 O(VE)? 因为 map 保证每个节点只创建一次,每条邻接关系只处理常数次。

八、加强记忆

克隆图的主线是“唯一映射 + 复制边”。Map<原节点, 新节点> 同时解决三件事:判断是否已访问、防止环导致死循环、确保共享邻居只克隆一次。DFS 写成递归返回克隆节点,BFS 写成队列逐层扩展;不管哪种写法,最后都要保证新图对象独立、结构一致。