Topics › Graphs › Problems on BFS/DFS
Clone Graph
LeetCode ↗ Java source ↗ Edit this article ↗
Jump to code:
Problem
You are given a reference to one node of a connected, undirected graph. Every node stores an integer val and a list neighbors of the nodes it is joined to. Return a deep copy of the whole graph: a brand-new set of nodes with the same values and the same connections, where no pointer in the copy leads back into the original.
On LeetCode the graph is described by an adjacency list in which adj[i] lists the neighbours of the node whose value is i + 1, and the node you receive is the one with value 1. An empty list means the graph is empty and the input node is null.
Constraints: 0 ≤ n ≤ 100, values are unique and lie in 1..n, there are no self-loops or repeated edges, and the graph is connected.
The Java file uses this node type:
static class Node {
int val;
List<Node> neighbors = new ArrayList<>();
Node(int val) { this.val = val; }
}
Example 1
adj = [[2,4],[1,3],[2,4],[1,3]]
answer: a new 4-cycle 1-2-3-4-1 with the same adjacency lists
Node 1 touches 2 and 4, node 2 touches 1 and 3, and so on. The copy must have four new node objects wired the same way.
Example 2
adj = [[]]
answer: a single new node with value 1 and no neighbours
There is no edge to follow, so the copy is one isolated node. (For adj = [] the answer is null.)
Two things make this harder than copying a linked list. First, cycles: a naive “copy this node, then recursively copy each neighbour” runs forever on 1 -> 2 -> 1, because nothing tells it that node 1 is already being copied. Second, sharing: in example 1, node 3 is reachable from both 2 and 4, and the copy must contain exactly one node 3, not two. Both problems have the same cure: remember, for every original node, the copy you made for it.
Approach 1: Two passes, find every node then wire the edges
Intuition. Split the job into “which nodes exist” and “how they are connected”. If every copy already exists before we add a single edge, cycles and shared neighbours stop being a problem: for any original edge a - b we simply look up the copies of a and b and connect them. So first walk the graph once to collect all original nodes, create one copy per node in a map original -> copy, and then walk the collected list a second time to fill the neighbour lists.
Algorithm.
- If the input is
null, returnnull. - BFS from the input node with a
seenset, appending each node to a listallas it is dequeued. - For each node in
all, putoriginal -> new Node(original.val)into the map. - For each original node, and for each of its neighbours
nb, appendcopy.get(nb)to the copy’s neighbour list. - Return the copy of the input node.
static Node twoPass(Node node) {
if (node == null) return null;
List<Node> all = new ArrayList<>();
Set<Node> seen = new HashSet<>();
Deque<Node> queue = new ArrayDeque<>();
seen.add(node);
queue.add(node);
while (!queue.isEmpty()) { // pass 1: find every original node
Node cur = queue.poll();
all.add(cur);
for (Node nb : cur.neighbors) {
if (seen.add(nb)) queue.add(nb);
}
}
Map<Node, Node> copy = new HashMap<>();
for (Node orig : all) copy.put(orig, new Node(orig.val));
for (Node orig : all) { // pass 2: copy each adjacency list
Node c = copy.get(orig);
for (Node nb : orig.neighbors) c.neighbors.add(copy.get(nb));
}
return copy.get(node);
}
Complexity. Time O(V + E): each pass touches every node once and every adjacency entry once. Space O(V) for seen, the list, the map and the queue, not counting the clone itself.
This is already optimal in big-O terms. Its only weakness is that it walks the graph twice and keeps an extra list and set. The next two approaches fold everything into one traversal, using the map as the visited set.
Approach 2: One-pass DFS with a map
Intuition. The map original -> copy can do double duty. If a node is in the map, it has been visited and its copy exists. So a recursive function dfs(node) can work like this: if the node is already in the map, return its copy; otherwise create the copy, store it, and fill its neighbour list by calling dfs on each neighbour.
The order of those last two steps is the whole trick. The copy must be stored in the map before recursing. When the recursion goes 1 -> 2 -> 1, the inner call for node 1 must find the half-built copy of 1 and return it immediately. If we stored the copy only after the loop, that inner call would create a second copy of node 1 and recurse again, forever.
Algorithm.
dfs(null)returnsnull.- If
nodeis in the map, return the stored copy. - Create
c = new Node(node.val)and storenode -> c. - For each neighbour
nb, appenddfs(nb)toc.neighbors. - Return
c.
static Node cloneDfs(Node node) {
return dfs(node, new HashMap<>());
}
static Node dfs(Node node, Map<Node, Node> copy) {
if (node == null) return null;
Node done = copy.get(node);
if (done != null) return done; // already copied: reuse it, this closes cycles
Node c = new Node(node.val);
copy.put(node, c); // register BEFORE recursing into neighbours
for (Node nb : node.neighbors) c.neighbors.add(dfs(nb, copy));
return c;
}
Dry run on example 1 (1:[2,4], 2:[1,3], 3:[2,4], 4:[1,3]; primes mark copies):
| step | call | what happens |
|---|---|---|
| 1 | dfs(1) | create 1’, store it, recurse into neighbour 2 |
| 2 | dfs(2) | create 2’; neighbour 1 is in the map, append 1’; recurse into 3 |
| 3 | dfs(3) | create 3’; neighbour 2 is in the map, append 2’; recurse into 4 |
| 4 | dfs(4) | create 4’; append 1’ and 3’ (both in the map); return 4’ |
| 5 | back in 3 | append 4’, so 3’ = [2’, 4’]; return 3’ |
| 6 | back in 2 | append 3’, so 2’ = [1’, 3’]; return 2’ |
| 7 | back in 1 | append 2’; neighbour 4 is in the map, append 4’; 1’ = [2’, 4’] |
Each original node produced exactly one copy, and every neighbour list keeps its original order.
Complexity. Time O(V + E): each node is created once, and each adjacency entry triggers one call that either returns from the map or does real work once. Space O(V) for the map plus O(V) recursion depth in the worst case (a long path).
Approach 3: One-pass BFS with a map
Intuition. The same map-as-visited idea works with a queue instead of the call stack, which removes any risk of a stack overflow on a long, path-like graph. Create a node’s copy the first time it is seen as somebody’s neighbour, and enqueue the original. When a node is later dequeued, its copy already exists, and so do the copies of any neighbours already seen; the ones not yet seen are created on the spot. Either way, appending the neighbour’s copy is always possible.
Algorithm.
- If the input is
null, returnnull. - Copy the input node, store it in the map, and enqueue the original.
- While the queue is not empty, dequeue
curand fetch its copyc. - For each neighbour
nb: if it has no copy yet, create one, store it, and enqueuenb. Append the copy ofnbtoc.neighbors. - Return the copy of the input node.
static Node cloneBfs(Node node) {
if (node == null) return null;
Map<Node, Node> copy = new HashMap<>();
copy.put(node, new Node(node.val));
Deque<Node> queue = new ArrayDeque<>();
queue.add(node);
while (!queue.isEmpty()) {
Node cur = queue.poll();
Node c = copy.get(cur);
for (Node nb : cur.neighbors) {
Node nbCopy = copy.get(nb);
if (nbCopy == null) { // first sighting: create it and schedule a visit
nbCopy = new Node(nb.val);
copy.put(nb, nbCopy);
queue.add(nb);
}
c.neighbors.add(nbCopy);
}
}
return copy.get(node);
}
Complexity. Time O(V + E): each node is enqueued once, and each adjacency entry costs one map lookup and one append. Space O(V) for the map and the queue.
Each undirected edge a - b is stored twice, once in a’s list and once in b’s list, and each of those two entries is copied once when its owner is dequeued. So the clone ends up with exactly the same edge set as the original, with no duplicates and none missing.
Edge cases and pitfalls
- Empty graph. A
nullinput must returnnull, not a node with value 0. - Register before you recurse. In the DFS, storing the copy after the neighbour loop causes infinite recursion on any cycle, and every undirected edge is already a cycle of length two.
- Key the map by node, not by value. The Java
Nodeclass does not overrideequalsorhashCode, so aHashMap<Node, Node>compares by identity, which is exactly right. LeetCode guarantees unique values in1..100, so an arrayNode[101]indexed byvalalso works, but a value-keyed table silently breaks if two nodes ever share a value. - Do not reuse original nodes. Appending
nbinstead of its copy to a clone’s list creates a “copy” that leaks into the original. The tests walk the whole clone and assert that none of its nodes is an original node. - Do not mutate the input. Some solutions mark visited nodes by changing
val. That breaks the caller’s graph; use the map instead. - Neighbour order. All three methods preserve each node’s neighbour order, which is what the LeetCode checker compares.
Related problems
- Clone a LL with random and next pointer uses the same original-to-copy map on a linked list with extra pointers.
- DFS covers the traversal that Approach 2 is built on.
- Number of islands(Do in Grid and Graph Both) uses the same visited-set discipline on a grid.
- Connected Components is what you would need to clone a graph that is not connected: start a traversal from every unvisited node.