Search
⌘K
Get Premium
Depth-First Search

Copy Graph

easy

DESCRIPTION

Given a reference to a variable node which is part of an undirected, connected graph, write a function to return a copy of the graph as an adjacency list in dictionary form. The keys of the adjacency list are the values of the nodes, and the values are the neighbors of the nodes.

node is an instance of the following class, where neighbors is a list of references to other nodes in the graph (also of type IntGraphNode):

class IntGraphNode:
    def __init__(self, value = 0, neighbors = None):
    self.value = value
    self.neighbors = neighbors if neighbors is not None else []

Example 1:

Input:

node = IntGraphNode(1, [IntGraphNode(2), IntGraphNode(3)])
321

Output:

>>> copy_graph(node)
{1: [2, 3], 2: [1], 3: [1]}

Example 2: Input:

n1 = IntGraphNode(1)
n2 = IntGraphNode(2)
n3 = IntGraphNode(3)
n4 = IntGraphNode(4)

n1.neighbors = [n2, n4]
n2.neighbors = [n1, n3]
n3.neighbors = [n2, n4]
n4.neighbors = [n1, n3]
4321

Output:

>>> copy_graph(n1)
{1: [2, 4], 2: [1, 3], 3: [2, 4], 4: [1, 3]}

Explanation

This solution uses depth-first search to traverse each node in the original graph. We can define a recursive helper function dfs that takes in an input node to help us perform the DFS traversal.
At each node, we:
  1. Add the value of the node as a key in the adjacency list, and a list of its neighbor's values as the value in the dictionary.
  2. Recursively call the dfs function on each neighbor of the node.
The adjacency list also helps us keep track of visited nodes. If we call dfs on a node that has already been added to the adjacency list, this means we have already visited the node, so we can return right away before making any more recursive calls.
Solution
# class IntGraphNode:
# value: int
# id: int
# neighbors: List[IntGraphNode]
def copy_graph(node):
adj_list = {}
def dfs(node):
if node.value in adj_list:
return
adj_list[node.value] = [n.value for n in node.neighbors]
for neighbor in node.neighbors:
dfs(neighbor)
if node:
dfs(node)
return adj_list
We can now take a closer look at the solution by visualizing each step as it traverses the graph below:
3210

Initialization

The first step is to initialize adj_list as an empty dictionary. We will return this dictionary at the end, after the depth-first search traversal is complete. We then define the recursive helper function dfs that takes a node as input, and make the initial call to dfs with the input node.
Visualization
# class IntGraphNode:
# value: int
# id: int
# neighbors: List[IntGraphNode]
def copy_graph(node):
adj_list = {}
def dfs(node):
if node.value in adj_list:
return
adj_list[node.value] = [n.value for n in node.neighbors]
for neighbor in node.neighbors:
dfs(neighbor)
if node:
dfs(node)
return adj_list
3210

copy graph

0 / 2

Defining both adj_list and the helper dfs function inside the main function ensures us that:
  • each call to the recursive function can access adj_list directly
  • the scope of adj_list is limited to the main function, which means that other parts of the code cannot modify it.
When the dfs function is called with a node, it first checks if the node is already present in the adj_list dictionary. If it isn't, it adds the node to the dictionary. The key is the value of the node, and the value is a list of the values of each of the node's neighbors.
Visualization
# class IntGraphNode:
# value: int
# id: int
# neighbors: List[IntGraphNode]
def copy_graph(node):
adj_list = {}
def dfs(node):
if node.value in adj_list:
return
adj_list[node.value] = [n.value for n in node.neighbors]
for neighbor in node.neighbors:
dfs(neighbor)
if node:
dfs(node)
return adj_list
def dfs(node):
if node.value in adj_list:
return
adj_list[node.value] = [n.value for n in node.neighbors]
for neighbor in node.neighbors:
dfs(neighbor)
3210adj_list{}

recursive call

0 / 1

Then, it recursively calls dfs on each neighbor of the original node.
Visualization
# class IntGraphNode:
# value: int
# id: int
# neighbors: List[IntGraphNode]
def copy_graph(node):
adj_list = {}
def dfs(node):
if node.value in adj_list:
return
adj_list[node.value] = [n.value for n in node.neighbors]
for neighbor in node.neighbors:
dfs(neighbor)
if node:
dfs(node)
return adj_list
def dfs(node):
if node.value in adj_list:
return
adj_list[node.value] = [n.value for n in node.neighbors]
for neighbor in node.neighbors:
dfs(neighbor)
3210adj_list{0: [1]}

add to adj_list

0 / 5

Recursive call to `dfs`
When dfs is called on a node that is already present in adj_list, it returns immediately without making any more recursive calls, which helps us avoid infinite loops in the graph. After returning, the function continues to the next neighbor of the current node.
Visualization
# class IntGraphNode:
# value: int
# id: int
# neighbors: List[IntGraphNode]
def copy_graph(node):
adj_list = {}
def dfs(node):
if node.value in adj_list:
return
adj_list[node.value] = [n.value for n in node.neighbors]
for neighbor in node.neighbors:
dfs(neighbor)
if node:
dfs(node)
return adj_list
def dfs(node):
if node.value in adj_list:
return
adj_list[node.value] = [n.value for n in node.neighbors]
for neighbor in node.neighbors:
dfs(neighbor)
def dfs(node):
if node.value in adj_list:
return
adj_list[node.value] = [n.value for n in node.neighbors]
for neighbor in node.neighbors:
dfs(neighbor)
def dfs(node):
if node.value in adj_list:
return
adj_list[node.value] = [n.value for n in node.neighbors]
for neighbor in node.neighbors:
dfs(neighbor)
3210adj_list{0: [1], 1: [0,2]}

recursive call

0 / 2

Returning from a previously visited node.
This process continues until all nodes in the original graph have been visited and added to the adj_list dictionary.

Animated Solution

|
list of neighbors
Try these examples:
Visualization
# class IntGraphNode:
# value: int
# id: int
# neighbors: List[IntGraphNode]
def copy_graph(node):
adj_list = {}
def dfs(node):
if node.value in adj_list:
return
adj_list[node.value] = [n.value for n in node.neighbors]
for neighbor in node.neighbors:
dfs(neighbor)
if node:
dfs(node)
return adj_list
3210

copy graph

0 / 26

Test Your Knowledge

Login to take the complexity quiz and track your progress

Complexity Analysis

Time Complexity: O(N + M) where N is the number of nodes and M is the number of edges in the graph for the depth-first search traversal.

Space Complexity: O(N + M) where N is the number of nodes and M is the number of edges in the graph. The space complexity is due to the adjacency list that stores the graph structure: each of the N nodes is stored once as keys, and each of the M edges is stored as part of the values in the dictionary.

Your account is free and you can post anonymously if you choose.

Unlock Premium Coding Content

Interactive algorithm visualizations
Reading Progress

On This Page