Depth First Search Operations
Node
Edge→
Run
Start
Target
100%
Ctrl + scroll to zoom
See DFS dive deep into each branch before backtracking through the graph.
Time O(V + E) · Space O(V)
from typing import Dict, Set, List, Any
def dfs(graph: Dict[Any, Set[Any]], start: Any) -> List[Any]:
"""
Perform DFS traversal starting from a given node (iterative).
Args:
graph: Adjacency list representation {node: {neighbors}}
start: Starting node for traversal
Returns:
List of nodes in DFS order (depth-first)
Raises:
ValueError: If start node is not in graph
"""
if start not in graph:
raise ValueError(f"Start node '{start}' not in graph")
visited = {start}
stack = [start] # Stack instead of queue - the key difference from BFS
order = []
while stack:
node = stack.pop() # LIFO: pop from end - O(1)
order.append(node)
for neighbor in graph[node]:
if neighbor not in visited:
visited.add(neighbor)
stack.append(neighbor)
return order
def dfs_recursive(graph: Dict[Any, Set[Any]], start: Any) -> List[Any]:
"""
Perform DFS traversal using recursion (uses call stack implicitly).
Args:
graph: Adjacency list representation
start: Starting node
Returns:
List of nodes in DFS order
"""
if start not in graph:
raise ValueError(f"Start node '{start}' not in graph")
order = []
visited = set()
def _dfs(node: Any) -> None:
visited.add(node)
order.append(node)
for neighbor in graph[node]:
if neighbor not in visited:
_dfs(neighbor)
_dfs(start)
return order
def dfs_find_path(graph: Dict[Any, Set[Any]], start: Any, target: Any) -> List[Any] | None:
"""
Find a path between two nodes using DFS.
Note: Unlike BFS, DFS does NOT guarantee the shortest path.
It finds ANY valid path, which may be longer than optimal.
Args:
graph: Adjacency list representation
start: Starting node
target: Target node to reach
Returns:
List of nodes representing a path, or None if no path exists
"""
if start not in graph:
raise ValueError(f"Start node '{start}' not in graph")
if target not in graph:
raise ValueError(f"Target node '{target}' not in graph")
if start == target:
return [start]
visited = {start}
stack = [(start, [start])] # (node, path_to_node)
while stack:
node, path = stack.pop() # LIFO: pop from end
for neighbor in graph[node]:
if neighbor not in visited:
new_path = path + [neighbor]
if neighbor == target:
return new_path
visited.add(neighbor)
stack.append((neighbor, new_path))
return None # No path found
# Example usage and demonstration
if __name__ == "__main__":
# Example graph (undirected, represented with bidirectional edges)
#
# A
# / \
# B C
# / \ \
# D E F
#
example_graph = {
"A": {"B", "C"},
"B": {"A", "D", "E"},
"C": {"A", "F"},
"D": {"B"},
"E": {"B"},
"F": {"C"},
}
print("DFS Traversal (iterative):", dfs(example_graph, "A"))
print("DFS Traversal (recursive):", dfs_recursive(example_graph, "A"))
print("DFS Path A->F:", dfs_find_path(example_graph, "A", "F"))
Depth-first search (DFS) explores a graph by going as deep as possible along each branch before backtracking. It uses a stack (or recursion) to remember which nodes to visit next. DFS is the foundation for many graph algorithms.
Start at the source node, mark it as visited. For each unvisited neighbor, recursively perform DFS. Backtrack when all neighbors have been visited. The implicit or explicit stack tracks the traversal path.