Depth-first search (DFS) explores a graph by going as far along one branch as possible before backtracking to explore alternative paths.
What is depth-first search?
Depth-first search (DFS) is a graph traversal technique that systematically explores all nodes in a graph by moving forward whenever possible and backtracking only when necessary. Unlike breadth-first search (BFS), which explores nodes level by level, DFS focuses on diving deep into a graph's structure, following one branch as far as it can go before retreating.
In DFS, once a node is visited, the algorithm continues to explore the graph by visiting an adjacent unvisited node. This process is repeated until it reaches a node with no unvisited neighbours. At this point, the algorithm backtracks — that is, it returns to the previous node to check for any remaining unexplored paths.
DFS can be used on various types of graphs including:
Directed graphs, where edges have direction.
Undirected graphs, where edges are bidirectional.
Connected graphs, where there is a path between any two nodes.
Disconnected graphs, where some nodes may not be reachable from others.
Practice Questions
FAQ
The order in which nodes are visited during a DFS traversal can vary significantly based on how the graph’s adjacency structure is stored and the specific logic used to select neighbouring nodes. For example, in an adjacency list, if neighbours are stored alphabetically or in the order they were added, this will influence which branch is explored first. Additionally, when manually implementing DFS, programmers may choose different methods to loop through neighbours (e.g. ascending or descending index order), which directly affects traversal paths. In recursive DFS, the order of recursive calls also dictates the path taken. This means that even on the same graph, two DFS implementations could produce different traversal orders, especially in graphs with branching nodes where multiple unvisited neighbours are present. These differences do not affect the algorithm’s correctness but can influence results in problems where the visiting order matters, such as maze-solving or output generation based on traversal sequence.
DFS can be modified to terminate early by introducing a check during traversal that compares the currently visited node to the target node. If the target is found, the algorithm can immediately stop further exploration by returning from the recursive call or breaking out of the iteration loop. This technique is particularly useful in search problems where the goal is not to explore the entire graph but to confirm whether a path exists or to retrieve a single instance of a solution. For example, when navigating a maze or solving a logic puzzle, it may be unnecessary and inefficient to continue searching once the solution has been found. Early termination reduces the time and space complexity in practice, even though the worst-case complexity remains the same. Implementing this optimisation can make DFS more responsive in real-time systems or interactive applications where timely results are crucial.
When performing DFS on graphs that may contain cycles, it is essential to prevent the algorithm from re-visiting nodes, which could lead to infinite loops and eventually a stack overflow or crash. The most effective strategy is to maintain a visited set or list that tracks which nodes have already been explored. Before visiting a neighbour, the algorithm checks if it is already in the visited set; if it is, it skips that node. In recursive implementations, this check is placed at the start of the DFS function. In iterative implementations using a stack, nodes are only pushed onto the stack if they have not yet been visited. It is also important that the visited set persists across recursive calls or iterations and is not reset. In undirected graphs, care should be taken to differentiate between revisiting the parent node and encountering a cycle. Robust DFS implementations always include such cycle detection mechanisms, especially in large or complex graphs.
Yes, DFS can be adapted to reconstruct the path taken from the starting node to the goal node by maintaining a parent mapping or predecessor dictionary during traversal. Each time a node is visited, the algorithm records the node from which it was discovered. Once the goal node is found, the full path can be reconstructed by tracing backwards from the goal to the start using the parent mapping. This technique is useful in applications such as puzzle solving or navigation systems where knowing the path is necessary, not just the fact that a path exists. In recursive implementations, this can be achieved by passing the parent node in the recursive call and recording it. In iterative DFS, the parent can be recorded when a node is pushed onto the stack. While DFS does not guarantee the shortest path, this method ensures the algorithm can still produce a valid path, which may be acceptable in scenarios where optimality is not required.
The maximum depth of recursion in a DFS traversal depends directly on the structure of the graph. In a tree or linear graph, where nodes are connected in a chain-like manner, DFS may reach a maximum depth equal to the total number of nodes. In more balanced or branching graphs, the depth is determined by the longest path from the root or start node to the deepest leaf or terminal node. Sparse graphs with long, narrow paths can result in very deep recursion, which may lead to stack overflow errors in languages or systems with limited recursion depth. Conversely, dense graphs or graphs with many short cycles often produce shallower recursion trees, as the algorithm quickly encounters already visited nodes and backtracks. Understanding this relationship is crucial for managing system resources during DFS execution, especially when dealing with very large graphs. In cases where the depth may become unmanageable, an iterative implementation with an explicit stack is generally preferred.
