15 DFS Interview Questions and Answers
Prepare for your technical interview with this guide on Depth-First Search (DFS), featuring common questions and detailed answers.
Prepare for your technical interview with this guide on Depth-First Search (DFS), featuring common questions and detailed answers.
Depth-First Search (DFS) is a fundamental algorithm used in computer science for traversing or searching tree and graph data structures. It explores as far as possible along each branch before backtracking, making it an essential technique for solving problems related to connectivity, pathfinding, and cycle detection. Understanding DFS is crucial for tackling complex data structures and algorithms, which are often a focal point in technical interviews.
This article provides a curated selection of DFS-related questions and answers to help you prepare effectively. By familiarizing yourself with these scenarios, you will gain a deeper understanding of DFS and enhance your problem-solving skills, making you more confident and proficient during your interview.
Depth-First Search (DFS) is a graph traversal algorithm that explores as far as possible along each branch before backtracking. In a recursive implementation, the algorithm uses the call stack to keep track of the vertices to be explored.
Here is the pseudocode for a recursive implementation of DFS:
function DFS(vertex, visited): mark vertex as visited for each neighbor of vertex: if neighbor is not visited: DFS(neighbor, visited)
In this pseudocode:
DFS
takes a vertex and a set of visited vertices as arguments.function DFS_iterative(graph, start_node): stack = [start_node] visited = set() while stack is not empty: node = stack.pop() if node not in visited: visit(node) visited.add(node) for neighbor in graph[node]: if neighbor not in visited: stack.append(neighbor)
The time complexity of DFS is O(V + E), where V is the number of vertices and E is the number of edges. This arises because DFS visits every vertex and traverses every edge in the graph.
DFS uses a stack data structure, either explicitly or through recursion, to keep track of the vertices to be explored. Each vertex is visited once, and each edge is considered once, leading to the time complexity being proportional to the sum of the number of vertices and edges.
The space complexity of DFS is determined by the maximum depth of the recursion stack or the maximum size of the stack used to store the nodes. In the worst-case scenario, the space complexity is O(V), where V is the number of vertices.
This is because DFS may need to store all the vertices in the recursion stack if the graph is a single long path. For example, in a tree structure, the maximum depth of the recursion stack will be equal to the height of the tree, which can be as large as the number of vertices.
To perform topological sorting on a Directed Acyclic Graph (DAG) using DFS, modify DFS to record the vertices in a specific order. Add each vertex to a stack when its DFS call is complete, ensuring that each vertex appears before any vertices it points to, achieving a topological order.
Example:
from collections import defaultdict class Graph: def __init__(self, vertices): self.graph = defaultdict(list) self.V = vertices def add_edge(self, u, v): self.graph[u].append(v) def topological_sort_util(self, v, visited, stack): visited[v] = True for i in self.graph[v]: if not visited[i]: self.topological_sort_util(i, visited, stack) stack.append(v) def topological_sort(self): visited = [False] * self.V stack = [] for i in range(self.V): if not visited[i]: self.topological_sort_util(i, visited, stack) return stack[::-1] g = Graph(6) g.add_edge(5, 2) g.add_edge(5, 0) g.add_edge(4, 0) g.add_edge(4, 1) g.add_edge(2, 3) g.add_edge(3, 1) print(g.topological_sort()) # Output: [5, 4, 2, 3, 1, 0]
Topological sorting is a linear ordering of vertices in a DAG such that for every directed edge u -> v, vertex u comes before vertex v. DFS can be used to perform topological sorting by visiting each node and marking it as visited, then recursively visiting all its adjacent nodes. Once all adjacent nodes are visited, the current node is pushed onto a stack. The stack will then contain the topologically sorted order of nodes.
Pseudocode:
function topologicalSort(graph): stack = empty stack visited = set() function dfs(node): if node in visited: return visited.add(node) for neighbor in graph[node]: dfs(neighbor) stack.push(node) for each node in graph: if node not in visited: dfs(node) return stack (in reverse order)
DFS can be used to find strongly connected components (SCCs) in a directed graph by leveraging Kosaraju’s algorithm, which involves two main passes of DFS.
Kosaraju’s algorithm works as follows:
Here is a concise example to illustrate the process:
from collections import defaultdict class Graph: def __init__(self, vertices): self.graph = defaultdict(list) self.V = vertices def add_edge(self, u, v): self.graph[u].append(v) def dfs(self, v, visited, stack): visited[v] = True for i in self.graph[v]: if not visited[i]: self.dfs(i, visited, stack) stack.append(v) def reverse(self): g = Graph(self.V) for i in self.graph: for j in self.graph[i]: g.add_edge(j, i) return g def fill_order(self, v, visited, stack): visited[v] = True for i in self.graph[v]: if not visited[i]: self.fill_order(i, visited, stack) stack.append(v) def get_sccs(self): stack = [] visited = [False] * self.V for i in range(self.V): if not visited[i]: self.fill_order(i, visited, stack) gr = self.reverse() visited = [False] * self.V while stack: i = stack.pop() if not visited[i]: gr.dfs(i, visited, []) print() g = Graph(5) g.add_edge(1, 0) g.add_edge(0, 2) g.add_edge(2, 1) g.add_edge(0, 3) g.add_edge(3, 4) g.get_sccs()
Kosaraju’s algorithm involves two main phases:
1. Perform a DFS on the original graph to compute the finishing times of each vertex.
2. Reverse the graph and perform a DFS in the order of decreasing finishing times to identify the SCCs.
Pseudocode:
function kosaraju(graph): stack = empty stack visited = set() # First DFS to compute finishing times for each vertex v in graph: if v not in visited: dfs_first(v, graph, visited, stack) # Reverse the graph reversed_graph = reverse(graph) # Second DFS to find SCCs visited.clear() sccs = empty list while stack is not empty: v = stack.pop() if v not in visited: scc = empty list dfs_second(v, reversed_graph, visited, scc) sccs.append(scc) return sccs function dfs_first(v, graph, visited, stack): visited.add(v) for each neighbor in graph[v]: if neighbor not in visited: dfs_first(neighbor, graph, visited, stack) stack.push(v) function dfs_second(v, reversed_graph, visited, scc): visited.add(v) scc.append(v) for each neighbor in reversed_graph[v]: if neighbor not in visited: dfs_second(neighbor, reversed_graph, visited, scc)
When applied to a maze, DFS can be used to find a path from the start point to the end point by exploring each possible path recursively.
Pseudocode to solve a maze using DFS:
function DFS(maze, start, end): stack = [start] visited = set() while stack is not empty: current = stack.pop() if current is end: return True if current not in visited: visited.add(current) for neighbor in get_neighbors(current, maze): if neighbor not in visited: stack.append(neighbor) return False function get_neighbors(position, maze): neighbors = [] for direction in [(0, 1), (1, 0), (0, -1), (-1, 0)]: new_position = (position[0] + direction[0], position[1] + direction[1]) if is_valid(new_position, maze): neighbors.append(new_position) return neighbors function is_valid(position, maze): x, y = position return 0 <= x < len(maze) and 0 <= y < len(maze[0]) and maze[x][y] != '#'
A bipartite graph is a graph whose vertices can be divided into two disjoint sets such that no two graph vertices within the same set are adjacent. To check if a graph is bipartite using DFS, we can attempt to color the graph using two colors and verify that no two adjacent vertices share the same color.
Pseudocode:
function isBipartite(graph): n = number of vertices in graph colors = array of size n, initialized to -1 function dfs(node, color): colors[node] = color for neighbor in graph[node]: if colors[neighbor] == -1: if not dfs(neighbor, 1 - color): return false elif colors[neighbor] == color: return false return true for i from 0 to n-1: if colors[i] == -1: if not dfs(i, 0): return false return true
Articulation points in a graph are vertices that, when removed, cause the graph to become disconnected. These points are important in network design and analysis because they represent vulnerabilities in the network.
DFS can be used to find articulation points by keeping track of the discovery and low times of each vertex. The discovery time is the time at which a vertex is first visited, and the low time is the lowest discovery time reachable from the vertex. By comparing these times, we can identify articulation points.
Here is a concise example to illustrate this:
from collections import defaultdict class Graph: def __init__(self, vertices): self.V = vertices self.graph = defaultdict(list) self.time = 0 def add_edge(self, u, v): self.graph[u].append(v) self.graph[v].append(u) def dfs(self, u, visited, parent, low, disc, ap): children = 0 visited[u] = True disc[u] = self.time low[u] = self.time self.time += 1 for v in self.graph[u]: if not visited[v]: parent[v] = u children += 1 self.dfs(v, visited, parent, low, disc, ap) low[u] = min(low[u], low[v]) if parent[u] is None and children > 1: ap[u] = True if parent[u] is not None and low[v] >= disc[u]: ap[u] = True elif v != parent[u]: low[u] = min(low[u], disc[v]) def find_articulation_points(self): visited = [False] * self.V disc = [float('Inf')] * self.V low = [float('Inf')] * self.V parent = [None] * self.V ap = [False] * self.V for i in range(self.V): if not visited[i]: self.dfs(i, visited, parent, low, disc, ap) for index, value in enumerate(ap): if value: print(index) g = Graph(5) g.add_edge(0, 1) g.add_edge(0, 2) g.add_edge(1, 2) g.add_edge(1, 3) g.add_edge(3, 4) g.find_articulation_points()
Articulation points, also known as cut vertices, are nodes in a graph that, when removed, increase the number of connected components. These points are useful for understanding the structure and resilience of a network. DFS can be used to find these points efficiently.
The algorithm involves the following key steps:
Here is the pseudocode to find articulation points using DFS:
function findArticulationPoints(graph): initialize discovery and low arrays initialize parent array initialize articulation point array for each vertex v in graph: if v is not visited: DFS(v) function DFS(v): mark v as visited set discovery[v] and low[v] to current time increment current time initialize child count to 0 for each neighbor u of v: if u is not visited: set parent[u] to v increment child count DFS(u) update low[v] to min(low[v], low[u]) if parent[v] is null and child count > 1: mark v as articulation point if parent[v] is not null and low[u] >= discovery[v]: mark v as articulation point else if u is not parent[v]: update low[v] to min(low[v], discovery[u])
One of the key implications of using a recursive approach for DFS is the risk of exceeding the maximum recursion depth, which can lead to a stack overflow. This is particularly problematic in large graphs or deep trees where the depth of recursion can be significant.
To handle potential issues with recursive stack depth, there are a few strategies:
Example of an iterative DFS implementation:
def dfs_iterative(graph, start): stack = [start] visited = set() while stack: vertex = stack.pop() if vertex not in visited: visited.add(vertex) stack.extend(set(graph[vertex]) - visited) return visited graph = { 'A': ['B', 'C'], 'B': ['A', 'D', 'E'], 'C': ['A', 'F'], 'D': ['B'], 'E': ['B', 'F'], 'F': ['C', 'E'] } dfs_iterative(graph, 'A') # Output: {'A', 'B', 'C', 'D', 'E', 'F'}
There are several pathfinding variants using DFS, each with its own applications:
DFS has several real-world applications across various fields: