Problem overview
The method cloneGraph receives node, node 1 of a connected, undirected graph, and returns a deep copy of the graph: the copy of node 1. The lesson provides the Node class, with a public int val, a List<Node> neighbors and a constructor Node(int val) that creates an empty neighbor list. Node i has the value i, and every node can be reached from node 1.
A deep copy is made of exactly n brand-new nodes with the same values. The neighbor list of each new node holds the new nodes of the original node's neighbors, in the same order. No node of the copy may be a node of the original graph. When the graph is empty, node is null and the method returns null.
The examples show a graph as adjacency lists, where entry i - 1 holds the values of node i's neighbors. For [[2, 4], [1, 3], [2, 4], [1, 3]], the graph is a square of four nodes (1-2-3-4-1), and the copy is a new square with the same neighbor order. For [[]] the graph is a single node with no neighbors, and the copy is one new node with value 1. For [] the method receives null and returns null.
The statement asks for O(V + E) time and O(V) extra space, with up to 50,000 nodes and 100,000 edges.
Approach: BFS and a map from original to copy
The difficulty is that graphs have cycles and shared neighbors. If you copy a node by copying each neighbor recursively, you run around a cycle forever, and a node that is a neighbor of two nodes would be copied twice. The fix is a HashMap from each original node to its copy. It does two jobs: it finds the copy of any node met so far, and its keys tell which nodes have been met already.
The search is a breadth-first search. A node's copy is made the first time the search meets the node, and that is also when the node joins the queue. A node that already has a copy is never copied or queued again, which stops the search from looping through a cycle. Every node in the queue already has its copy. What the copy still lacks is its neighbor list, which is filled in when the node leaves the queue: for each neighbor, append the neighbor's copy to the list of the current node's copy.
Since the neighbors are visited in the original order, each copied list keeps that order. A recursive DFS with the same map would work as well. The BFS version uses a queue instead of the call stack, which is safer on a path of 50,000 nodes.
Step by step
- Handle the empty graph. If
nodeisnull, returnnull. The next lines readnode.val, which needs a node. - Create
copies, aMap<Node, Node>backed by aHashMap.Nodedoesn't defineequalsorhashCode, so the map compares nodes by identity: two different nodes are two different keys, even with the same value. - Put the first entry:
nodemaps tonew Node(node.val). - Create
queue, aDeque<Node>backed by anArrayDeque. It holds nodes whose neighbors still have to be copied. - Add
nodeto the queue. - Loop while the queue isn't empty. The search ends when every reachable node has been taken from the queue.
- Take the front node with
pollintocurrent. Now its copy's neighbor list gets filled. - Loop over
current.neighborswith an enhancedfor. Over the whole run each edge is looked at twice, once from each end. - Check
!copies.containsKey(neighbor): no copy yet means the search meets this node for the first time. - Make the copy: put
neighborincopies, mapped tonew Node(neighbor.val). - Add
neighborto the queue. A node joins the queue only when its copy is made, so each node is queued once. - Link the copies after the
if, still inside the loop: addcopies.get(neighbor)tocopies.get(current).neighbors. The copy ofcurrentpoints to the copy ofneighbor, never toneighboritself. - Return
copies.get(node), the copy of node 1.
The solution in Java
class Solution {
public Node cloneGraph(Node node) {
if (node == null) {
return null;
}
Map<Node, Node> copies = new HashMap<>();
copies.put(node, new Node(node.val));
Deque<Node> queue = new ArrayDeque<>();
queue.add(node);
while (!queue.isEmpty()) {
Node current = queue.poll();
for (Node neighbor : current.neighbors) {
if (!copies.containsKey(neighbor)) {
copies.put(neighbor, new Node(neighbor.val));
queue.add(neighbor);
}
copies.get(current).neighbors.add(copies.get(neighbor));
}
}
return copies.get(node);
}
}
The Java details worth a closer look:
- Identity keys. Without
equalsandhashCodeoverrides, a class inheritsObject's versions, which compare references. TwoNodeobjects with the samevalare two different keys: putting two such nodes in aHashMapleaves two entries. That is exactly what this algorithm needs, because the key must be the original node object, not its value. DequeandArrayDeque.addputs a node at the back andpolltakes the one at the front, which makes a first-in, first-out queue.ArrayDequeis the usual choice and doesn't acceptnullelements.pollreturnsnullwhen the queue is empty, which is why the loop testsisEmpty()first.- Diamond operator.
new HashMap<>()lets the compiler fill in the type arguments from the declared type. The lesson's editor already providesjava.util. In a file of your own, addimport java.util.*;. - References.
copies.get(current)returns the same copy object every time, so adding to itsneighborslist changes that one node. Nothing is duplicated by accident. - Declare against the interface. Using
MapandDequeas the variable types keeps the code independent of the concrete classes.
Complexity
Each node is copied once and queued once. Each edge is examined twice, once from each end, and each examination does a constant number of hash lookups on average. The time is O(V + E). The map holds one entry per node, and the queue never holds more than V nodes, so the extra space is O(V), not counting the copy itself.
Edge cases and common mistakes
- Empty graph. A
nullinput must returnnull. Skipping the check throws aNullPointerExceptiononnode.val. - A single node. The loop runs once with no neighbors, and the result is one new node with an empty list.
- Linking to the original neighbor. Writing
neighbors.add(neighbor)instead ofcopies.get(neighbor)builds a copy that still points into the original graph, which the lesson's tests report. - Copying a node twice. Without the
containsKeycheck, nodes reachable by two paths get two copies, and cycles never end. - Checking the map but not queueing. A node that gets a copy but is never added to the queue ends up with an empty neighbor list.
- Neighbor order. The copies' lists must keep the original order. Iterating the original list in order and appending guarantees that.
Testing
These results come from running the solution on graphs written as adjacency lists. In every case the copy had the same neighbor lists and contained none of the original nodes, and a path of 50,000 nodes, the largest size the statement allows, was copied in full.
| Adjacency list | Neighbors in the copy |
|---|---|
[[2, 4], [1, 3], [2, 4], [1, 3]] | [[2, 4], [1, 3], [2, 4], [1, 3]] |
[[]] | [[]] |
[] | [] (the method returned null) |
[[2, 3], [1, 3], [1, 2]] | [[2, 3], [1, 3], [1, 2]] |
[[3, 2], [1], [1]] | [[3, 2], [1], [1]] |
