DSAPrep
MediumGraphs

Clone Graph

Given a reference of a node in a connected undirected graph, return a deep copy (clone) of the graph.

Each node in the graph contains a value (int) and a list (List[Node]) of its neighbors: class Node { public int val; public List<Node> neighbors; }

Test case format: each node's value is the same as its index (1-indexed). The graph is represented in the test case using an adjacency list, where each inner list is the set of neighbor values for that node. The given node is always the first node with val == 1. You must return the copy of the given node as a reference to the cloned graph.

Example 1

Input: adjList = [[2,4],[1,3],[2,4],[1,3]]
Output: [[2,4],[1,3],[2,4],[1,3]]
Explanation: There are 4 nodes. Node 1's neighbors are 2 and 4; node 2's neighbors are 1 and 3; node 3's neighbors are 2 and 4; node 4's neighbors are 1 and 3.

Example 2

Input: adjList = [[]]
Output: [[]]
Explanation: The graph has one node with val = 1 and no neighbors.

Example 3

Input: adjList = []
Output: []
Explanation: An empty graph.

Constraints

  • The number of nodes in the graph is in the range [0, 100].
  • 1 <= Node.val <= 100
  • Node.val is unique for each node.
  • There are no repeated edges and no self-loops.
  • The graph is connected and all nodes can be visited starting from the given node.
View original on LeetCode β†—

The core difficulty isn’t traversal, it’s bookkeeping: while walking the original graph we need a way to look up β€œhave I already made a copy of this node?” so cycles (a graph, not a tree) don’t send us into infinite recursion, and so every clone points at the same clone of a shared neighbor rather than a fresh duplicate. A hash map from original node β†’ cloned node solves both problems at once.

Consider this tiny graph as a running example (adjacency list, 1-indexed):

Node Neighbors
1 2, 4
2 1, 3
3 2, 4
4 1, 3

DFS with a Clone Map

Time O(V + E)Space O(V + E)

Recurse from the start node. Before recursing into a neighbor, check the map: if we have already begun cloning that node, reuse the existing clone instead of recursing again (this is what breaks cycles).

class Node:
def __init__(self, val=0, neighbors=None):
self.val = val
self.neighbors = neighbors if neighbors is not None else []
class Solution:
def cloneGraph(self, node: 'Node') -> 'Node':
if not node:
return None
cloned = {} # original node -> its clone
def dfs(n):
if n in cloned:
return cloned[n]
copy = Node(n.val)
cloned[n] = copy # register BEFORE recursing, so cycles terminate
for neighbor in n.neighbors:
copy.neighbors.append(dfs(neighbor))
return copy
return dfs(node)

Watch the clone map: a clone is registered the instant it is created, and every neighbor lookup that hits an already-registered entry is the recursion guard stopping a re-descent. The left panel is the original graph, the middle column is the clone map, and the right panel grows into the deep copy (indigo = current dfs call, amber = the neighbor being looked up, slate = already cloned, emerald = fresh clone).

Original graph

1
2
3
4

Clone map

{ }

Clone graph

no clones yet
1 / 10
current dfs callneighbor being looked upalready cloned (map hit)fresh clone

Start at node 1. The join between the panels is the clone map: original β†’ clone. It is empty right now β€” and this graph is cyclic, so any naive re-walk would never terminate. The map is the bookkeeping that stops the recursion.

Every node is registered in the map the instant its clone is created, so re-entering it (because the graph has a cycle) is a cheap lookup, not a re-clone. Correctness: each original node maps to exactly one clone, and every edge in the original graph is mirrored between the corresponding clones. Complexity: each node is cloned once and each edge is traversed once β†’ O(V + E) time; the map and the recursion stack both hold up to V entries β†’ O(V + E) space counting the output graph itself.

BFS with a Clone Map

OptimalTime O(V + E)Space O(V + E)

Same idea, iterative: seed the map with a clone of the start node, then process nodes from a queue. This avoids recursion depth entirely, which matters if the graph is large or has a long path before the last unvisited node.

from collections import deque
class Solution:
def cloneGraph(self, node: 'Node') -> 'Node':
if not node:
return None
cloned = {node: Node(node.val)}
queue = deque([node])
while queue:
curr = queue.popleft()
for neighbor in curr.neighbors:
if neighbor not in cloned:
cloned[neighbor] = Node(neighbor.val)
queue.append(neighbor)
cloned[curr].neighbors.append(cloned[neighbor])
return cloned[node]

Same trace, BFS order: start by cloning 1 and enqueuing it. Pop 1, see neighbors 2 and 4 β€” both unseen, so clone and enqueue both, and wire 1'.neighbors = [2', 4']. Pop 2, see neighbors 1 (already cloned, just wire the edge) and 3 (clone, enqueue, wire). Pop 4, see neighbors 1 (wire) and 3 (already cloned by now, just wire). Pop 3, see neighbors 2 and 4 (both already cloned, just wire both edges). Queue empties with all 4 nodes cloned and every edge mirrored.

Why it’s the preferred version: identical time and space complexity to the DFS version, but the traversal is explicit (a queue) instead of hidden in the call stack, so there’s no risk of hitting Python’s recursion limit on graphs with long chains. Complexity: every node is enqueued once and every edge is inspected once β†’ O(V + E) time, O(V + E) space for the map, queue, and output graph.