Given a graph represented as an adjacency list, write a function to perform a Depth-First Search (DFS) traversal starting from a given node. The function should print each visited node. Also, provide an example graph and demonstrate how your function would traverse it. Explain the time and space complexity of your solution.
For example, consider the following graph:
{
0: [1, 2],
1: [2],
2: [0, 3],
3: [3]
}
If we start the DFS traversal from node 2, the expected output would be: 2, 0, 1, 3 (or another valid DFS traversal order).
Your function should:
This response provides a solution to the interview question about implementing Depth-First Search (DFS) traversal on a graph represented as an adjacency list.
Given a graph represented as an adjacency list, write a function to perform a Depth-First Search (DFS) traversal starting from a given node. The function should print each visited node. Also, provide an example graph and demonstrate how your function would traverse it. Explain the time and space complexity of your solution.
For example, consider the following graph:
{
0: [1, 2],
1: [2],
2: [0, 3],
3: [3]
}
If we start the DFS traversal from node 2, the expected output would be: 2, 0, 1, 3 (or another valid DFS traversal order).
A naive solution would involve recursively visiting each node in the graph, printing the node's value as it's visited. A visited
set is required to avoid infinite loops caused by cycles in the graph. If the graph is not connected, the naive approach will only traverse the connected component containing the start node.
def dfs_naive(graph, start_node, visited=None):
if visited is None:
visited = set()
print(start_node, end=" ")
visited.add(start_node)
for neighbor in graph[start_node]:
if neighbor not in visited:
dfs_naive(graph, neighbor, visited)
# Example Usage:
graph = {
0: [1, 2],
1: [2],
2: [0, 3],
3: [3]
}
print("Naive DFS traversal starting from node 2:")
dfs_naive(graph, 2)
print()
The provided naive solution is already reasonably optimal for DFS traversal. The core logic of recursively visiting nodes and using a visited
set is fundamental to DFS. Further optimization might involve using an iterative approach with a stack to avoid recursion depth limits for very large graphs, but that would add complexity without changing the fundamental time complexity.
def dfs(graph, start_node):
visited = set()
def dfs_recursive(node):
print(node, end=" ")
visited.add(node)
for neighbor in graph[node]:
if neighbor not in visited:
dfs_recursive(neighbor)
dfs_recursive(start_node)
# Example Usage:
graph = {
0: [1, 2],
1: [2],
2: [0, 3],
3: [3]
}
print("DFS traversal starting from node 2:")
dfs(graph, 2)
print()
def dfs_iterative(graph, start_node):
visited = set()
stack = [start_node]
while stack:
node = stack.pop()
if node not in visited:
print(node, end=" ")
visited.add(node)
# Add neighbors to the stack in reverse order to maintain the same traversal order as the recursive version
neighbors = list(graph[node])
for neighbor in reversed(neighbors):
stack.append(neighbor)
print("Iterative DFS traversal starting from node 2:")
dfs_iterative(graph, 2)
print()
visited
set stores each node that has been visited, which can grow up to the number of nodes in the graph in the worst case.KeyError
.visited
set correctly handles cycles in the graph, preventing infinite loops.