Depth-first search (DFS) is one of those algorithms you'll see everywhere once you start looking: tree traversals, compilers, maze solvers, dependency resolution, finding Strongly Connected Components (SCCs).
Depth-first search (DFS) is one of those algorithms youâll see everywhere once you start looking: tree traversals, compilers, maze solvers, dependency resolution, finding Strongly Connected Components (SCCs)âthe list goes on.
Hereâs the core idea:
Start somewhere, keep going deeper until you canât, then backtrack and try the next option.
Thatâs it. This âgo deep, then backtrackâ pattern is so fundamental that once you really get it, a whole bunch of graph and tree problems suddenly click into place.
đ Real-World Analogy â Exploring a Maze
Think about exploring a huge maze. Your strategy is simple:
If youâve worked with trees, youâve probably already used DFS without even thinking about it. Those standard traversalsâpreorder, inorder, and postorder? Theyâre all just DFS with different âvisit timings.â
Hereâs an interactive tree DFS visualization. Try switching between traversal types to see how the order changes.
Visit the current node before its children. Useful for copying trees.
Visit left subtree, then root, then right. For BST, gives sorted order.
Visit children before the root. Useful for deleting trees.
Quick note: When youâre working with trees, you donât strictly need a visited set because trees donât have cycles. But if youâre representing the tree as an undirected graph, youâll want to make sure you donât immediately traverse back to the parent node. For general graphs? A visited set is absolutely mandatory.
Key Concept: DFS explores deep along one path before trying alternatives. It must track visited nodes in graphs to avoid infinite loops caused by cycles.
For a graph G = (V, E) and a starting node s, the recursive DFS template looks like this:
s as visited.v of s:v isnât visited, recursively DFS from v.In pseudo-code:
dfs(u):
visited[u] = true
for each v in adj[u]:
if not visited[v]:
dfs(v)
This simple pattern is the backbone for:
Pretty versatile, right?
Letâs make this concrete with a small graph.
Try clicking a start node (if supported) and watch:
Consider this undirected graph:
0: 1, 2
1: 0, 2
2: 0, 1, 3, 4
3: 2
4: 2
Letâs run DFS starting at 0, processing neighbors in ascending numerical order:
0 visited. Order: [0].1 visited. Order: [0, 1].2 visited. Order: [0, 1, 2].3 visited. Order: [0, 1, 2, 3].4 visited. Order: [0, 1, 2, 3, 4].Different neighbor orderings will give you different (but equally valid) DFS traversal orders.
Weâll implement recursive DFS using an adjacency list for an unweighted graph. Weâll also handle disconnected graphs by iterating over all vertices to make sure we visit every component.
import java.util.*;
public class DFSExample {
static void dfs(int u, List<List<Integer>> adj, boolean[] visited, List<Integer> order) {
visited[u] = true;
order.add(u);
for (int v : adj.get(u)) {
if (!visited[v]) {
dfs(v, adj, visited, order);
}
}
}
// Runs DFS for all components and returns overall traversal order
static List<Integer> dfsAllComponents(List<List<Integer>> adj) {
int n = adj.size();
boolean[] visited = new boolean[n];
List<Integer> order = new ArrayList<>();
for (int i = 0; i < n; i++) {
if (!visited[i]) {
dfs(i, adj, visited, order);
}
}
return order;
}
public static void main(String[] args) {
// Example graph:
// 0: 1,2
// 1: 0,2
// 2: 0,1,3,4
// 3: 2
// 4: 2
int n = 5;
List<List<Integer>> adj = new ArrayList<>();
for (int i = 0; i < n; i++) {
adj.add(new ArrayList<>());
}
// Undirected edges
addEdge(adj, 0, 1);
addEdge(adj, 0, 2);
addEdge(adj, 1, 2);
addEdge(adj, 2, 3);
addEdge(adj, 2, 4);
List<Integer> order = dfsAllComponents(adj);
System.out.println("DFS order: " + order);
}
static void addEdge(List<List<Integer>> adj, int u, int v) {
adj.get(u).add(v);
adj.get(v).add(u);
}
} Instead of relying on recursion (which uses the system call stack), you can maintain your own explicit stack. This is especially useful for really deep graphs where recursion might cause a StackOverflowError.
stack = [start]
visited = set()
while stack not empty:
u = stack.pop()
if u is not visited:
mark u visited
push all unvisited neighbors of u onto stack
Pro tip: To exactly replicate the order of a recursive DFS, you should push neighbors onto the stack in reverse order.
Fun fact: a DFS-like method was used way back in the 19th century by a guy named Trémaux to solve mazes.
DFS will find a path from start to goal if one existsâbut it wonât necessarily find the shortest one.
Try:
This approach works great when:
If you need the shortest path in an unweighted grid or graph, youâll want BFS (Breadth-First Search), not DFS.
If youâre more of a visual learner, hereâs a solid short explanation of DFS on graphs:
For an explicit graph with V vertices and E edges:
Time Complexity: O(V + E)
Each vertex and each edge gets processed at most once (twice in undirected graphs, once per direction).
Space Complexity: O(V)
You need this for the visited array and the recursion stack (or explicit stack).
For implicit graphs (like game trees) where neighbors are generated on the fly:
b is the branching factor and d is the depth.When you run DFS on a directed graph, youâre implicitly building a DFS tree/forest. Edges can be classified as:
In an undirected graph, every non-tree edge is effectively a back edgeâforward and cross edges donât exist in that context.
These classifications are super useful for:
s, which nodes can I reach?âdfsAllComponents helper does.â ïž Common DFS Pitfalls
visited: Skip this and youâll get infinite recursion on cyclic graphs. Not fun.visited too late: Youâve got to mark a node as visited before recursively calling its neighbors. If you mark it after the call returns, other branches can revisit the same node unnecessarily.dfs(start_node) only visits the component containing start_node. Loop through all nodes to cover the entire graph.Use DFS when:
Use BFS when:
DFS is one of those fundamental tools youâll keep coming back to:
Once youâre comfortable with the DFS patternâmanaging visited sets, recursion/stacks, and entry/exit timesâa whole bunch of graph and tree problems become way more approachable.