HARD Lab · Graphs · 42

133. Clone Graph

Deep-copy an undirected graph by running a BFS/DFS from the entry node, using a single Map<original, clone> that simultaneously prevents infinite loops and acts as the clone registry — so every node is created exactly once and every edge is wired exactly once.

MediumBFS/DFSHash MapTypeScript

PROBLEM What we're solving

Given a reference to any node in a connected undirected graph, return a deep copy of the entire graph — new node objects, same structure. The example graph is a 4-node cycle: 1 — 2 — 3 — 4 — 1. Each node also connects to its "opposite" (1–3, 2–4). Cloning means four new Node objects whose neighbors arrays mirror the original topology. The catch: cycles mean naively following neighbors loops forever.

KEY IDEA One map, two jobs

Insight → maintain a Map<original, clone>. Before visiting a neighbor, check the map: if the neighbor is already a key, it has been cloned — reuse that clone. This check does two things at once: it prevents re-enqueueing visited nodes (cycle safety), and it hands back the correct clone object to wire into the current node's neighbor list. You never need a separate visited set.

RECIPE BFS clone, step by step

  • 0 · Null check. If the input node is null, return null.
  • 1 · Bootstrap. Create the clone of the start node, add it to cloneMap, and enqueue the original start node.
  • 2 · BFS loop. Dequeue an original node cur. Retrieve its clone from the map.
  • 3 · Wire neighbors. For each neighbor of cur: if it's not in the map yet, create its clone, enqueue the original, and add it to the map. Either way, push cloneMap.get(neighbor)! into cur's clone.neighbors.
  • 4 · Return. cloneMap.get(startNode)! is the root of the new graph.
Classic confusion → learners often add a separate visited set alongside the map — it's redundant. Membership in the map IS the visited check. If cloneMap.has(neighbor)is true, you've already created its clone and enqueued it; no need for a second data structure.

COST Complexity & alternatives

No visited tracking
∞ loop
Cycles cause infinite recursion/enqueueing.
BFS + cloneMap
O(V + E)
Each node and edge visited once; O(V) extra space for the map.

DFS alternative

DFS with the same map works identically — replace the queue with a stack (or use recursion). Recursive DFS is the most compact form: if (cloneMap.has(node)) return cloneMap.get(node)!; at the top of the function, then clone, recurse on neighbors, return.

Pattern transfer → the "map as visited + registry" trick appears in Copy List with Random Pointer (random pointers need the same clone-lookup), Graph Valid Tree, Number of Connected Components, and any problem that deep-copies a graph-like structure. Whenever you need to copy a structure with back-references, reach for a node→copy map.

RUN IT BFS clone — map doubles as visited set and clone registry

step 0 / 14
STARTGraph has 4 nodes. Start BFS from node 1. We use a Map<original, clone> that does double duty: prevents revisiting AND returns the existing clone.
1234
State
{—}
visited/cloneMap
[ ]
queue
current nodecloned (in map)done
slowfast

TYPESCRIPT The solution, annotated

cloneGraph.ts
class _Node {
  val: number;
  neighbors: _Node[];
  constructor(val = 0, neighbors: _Node[] = []) {
    this.val = val;
    this.neighbors = neighbors;
  }
}

function cloneGraph(node: _Node | null): _Node | null {
  if (!node) return null;

  // Map from original node → its deep clone.
  // It serves two roles at once: visited set AND clone registry.
  const cloneMap = new Map<_Node, _Node>();

  const queue: _Node[] = [node];
  cloneMap.set(node, new _Node(node.val));

  while (queue.length > 0) {
    const cur = queue.shift()!;
    const curClone = cloneMap.get(cur)!;

    for (const neighbor of cur.neighbors) {
      if (!cloneMap.has(neighbor)) {
        // First visit: create the clone and enqueue the original.
        cloneMap.set(neighbor, new _Node(neighbor.val));
        queue.push(neighbor);
      }
      // Whether brand-new or already seen, wire the edge.
      curClone.neighbors.push(cloneMap.get(neighbor)!);
    }
  }

  return cloneMap.get(node)!;
}

Reading it block by block

Lines 13–14 — null guard. An empty graph is valid input. Returning nullearly also means we never have to handle the "start node missing from map" edge case below.
Lines 17–19 — bootstrap. We create the clone of node before the loop so the map is never empty when we enter the BFS. The original node is enqueued so its neighbors get wired in the first iteration.
Lines 21–24 — dequeue and look up. queue.shift() gives us an original node. cloneMap.get(cur)! is safe because we only enqueue a node the moment we add it to the map — so a dequeued node is always in the map.
Lines 26–30 — first visit: create + enqueue.If a neighbor has no entry in the map, it hasn't been seen yet. Create its clone, register it, and enqueue the original so its own neighbors get processed later.
Line 32 — wire the edge (always). Regardless of whether this neighbor was brand-new or already cloned, we push its clone into curClone.neighbors. Moving this line outside the if block is the key simplification — no duplicated push.
Line 36 — return the start clone. After the loop every node is cloned and every edge wired. cloneMap.get(node)! is the entry point of the new graph.
Complexity → O(V + E) time — each node is enqueued once (map check prevents re-enqueueing) and each edge is traversed once. O(V) extra space for the map plus the BFS queue, which holds at most O(V) nodes.

INTERVIEWFollow-ups they'll ask

  • "What changes for a directed graph?"Nothing structural — BFS/DFS works the same; edges are just one-directional, so you don't risk double-wiring.
  • "Can you do it with DFS / recursion?" Yes. A recursive DFS is even more compact: if (map.has(n)) return map.get(n)!; as the base case, then clone, recurse each neighbor, return.
  • "What if the graph is disconnected?"The problem guarantees a connected graph, but if not you'd iterate over all nodes as potential starting points, cloning each component.
  • "Copy List with Random Pointer — how is it similar?" Identical pattern: a Map<OldNode, NewNode> handles both the next-pointer and the random-pointer wiring in two passes (or one DFS pass).
  • "What if node values aren't unique?" The map keys on object identity (reference), not value — so duplicate values are no problem. You cannot use node.val as the key.

MNEMONIC The one-liner

"Check the map before you clone — in the map means already done. Wire the edge either way."

TRIGGERS When you see ___ → reach for ___

deep-copy a graph or linked structure with back-referencesMap<original, clone>
BFS/DFS on a graph that may have cyclescloneMap doubles as visited set
Copy List with Random Pointersame node→copy map trick
need to return an existing copy, not create a duplicatemap.has() guard before new Node()

SKELETON The reusable shape

skeleton.ts
function cloneGraph(node: _Node | null): _Node | null {
  if (!node) return null;
  const cloneMap = new Map<_Node, _Node>();
  const queue: _Node[] = [node];
  cloneMap.set(node, new _Node(node.val));
  while (queue.length > 0) {
    const cur = queue.shift()!;
    for (const nb of cur.neighbors) {
      if (!cloneMap.has(nb)) {
        cloneMap.set(nb, new _Node(nb.val));
        queue.push(nb);
      }
      cloneMap.get(cur)!.neighbors.push(cloneMap.get(nb)!);
    }
  }
  return cloneMap.get(node)!;
}

FLASHCARDS Tap to flip

Why does cloneMap serve as the visited set?
A node is added to the map the instant its clone is created. So cloneMap.has(neighbor)is true iff we've already cloned it — no separate visited set needed.
tap to flip
1 / 7
Answer to reveal explanations. No penalty for retries.Score: 0 / 7
QUESTION 01
What is the time complexity of the BFS clone approach?
QUESTION 02
You are processing node 2 and find its neighbor 4 already in cloneMap. What should you do?
QUESTION 03
Why is a separate "visited" Set unnecessary alongside the cloneMap?
QUESTION 04
Trace the 4-node cycle 1: 2,4; 2: 1,3; 3: 2,4; 4: 1,3. After fully processing node 1 (dequeued first), which nodes are in the clone map?
QUESTION 05
Which problem uses the exact same "Map<original, copy>" pattern?
QUESTION 06
Why must you key the cloneMap on the node object (reference), not node.val?
QUESTION 07
What is returned when the input node is null?