Depth-First Search
Depth-first search explores a graph the way you explore a maze: pick a corridor, keep going deeper, and only walk back when you hit a dead end. It is the recursion-shaped traversal - the call stack quietly remembers the whole way back. That simple loop of "go deep, back up, try the next branch" powers cycle detection, topological sort, maze generation, and every "is there any path?" question. BFS asks "what is closest?"; DFS asks "where can I reach?".
Watch it dive and back up
Press play - or drag the timeline and step through it yourself.Depth-first search from A: always go deeper if you can, back up only when stuck. The stack remembers the way back. Neighbours are tried in alphabetical order.
Complexity
A maze and a ball of string
You enter a maze holding a ball of string, and you tie the end at the entrance. At every junction you take the first corridor you have not tried yet, unrolling string as you walk, and you chalk-mark every junction you visit. Dead end? Follow the string back to the last junction and take its next untried corridor. If a corridor leads to a chalked junction that your string already runs through, you have walked in a circle - that is a cycle. The string is the call stack: it always knows the way back, one junction at a time. When you are back at the entrance with no corridors left, you have seen the whole maze.
How it works, step by step
Start at a node. Mark it visited and give it discovery number 1.
Look at its neighbours in order. Go to the first unvisited one right away - do not finish the list first.
Repeat from the new node: always go deeper while any unvisited neighbour exists. The path you are standing on is the stack.
An edge that leads to a node already on the stack is a back edge - it closes a cycle. Note it, but do not walk it.
No unvisited neighbours left? Backtrack: pop the stack, step back one node, and try its next neighbour.
When you pop the start node, you are done. Every node and edge is handled once - O(V + E).
The code, in JavaScript
The natural form: the function call stack IS the algorithm's stack. Going deeper is a call; backtracking is a return. This is the graph from the animation above.
function dfs(graph, start) {
const visited = new Set();
const order = [];
function explore(node) {
visited.add(node); // mark BEFORE recursing, or a cycle loops forever
order.push(node);
for (const next of graph[node] ?? []) {
if (!visited.has(next)) {
explore(next); // go deeper NOW - the rest of the list waits
}
}
} // returning from explore() = backtracking
explore(start);
return order;
}
const graph = {
A: ["B", "C"], B: ["A", "D", "E"], C: ["A", "F", "G"],
D: ["B", "H"], E: ["B", "H"], F: ["C"], G: ["C"], H: ["D", "E"],
};
dfs(graph, "A"); // → ["A", "B", "D", "H", "E", "C", "F", "G"]Dry run: DFS from node A
The same steps the visualizer plays, written as a table. Generated by running the real algorithm - not written by hand.
| Step | Depth | What happened |
|---|---|---|
| 1 | 1 | Visit A and mark it #1. Stack: [A]. |
| 2 | 2 | Go deeper to B (#2). Stack: [A, B]. |
| 3 | 3 | Go deeper to D (#3). Stack: [A, B, D]. |
| 4 | 4 | Go deeper to H (#4). Stack: [A, B, D, H]. |
| 5 | 5 | Go deeper to E (#5). Stack: [A, B, D, H, E]. |
| 6 | 5 | E sees B - but B is already on my path (the stack). That is a cycle: B-D-H-E-B. Do not walk it. |
| 7 | 4 | E has no unvisited neighbours - back up to H. Stack: [A, B, D, H]. |
| 8 | 3 | H has no unvisited neighbours - back up to D. Stack: [A, B, D]. |
| 9 | 2 | D has no unvisited neighbours - back up to B. Stack: [A, B]. |
| 10 | 2 | B still has an edge to E - but E is already fully explored. Nothing new that way. |
| 11 | 1 | B has no unvisited neighbours - back up to A. Stack: [A]. |
| 12 | 2 | A tries its next branch - go deeper to C (#6). Stack: [A, C]. |
| 13 | 3 | Go deeper to F (#7). Stack: [A, C, F]. |
| 14 | 2 | F is a dead end - back up to C. Stack: [A, C]. |
| 15 | 3 | C tries its next branch - go deeper to G (#8). Stack: [A, C, G]. |
| 16 | 2 | G is a dead end - back up to C. Stack: [A, C]. |
| 17 | 1 | C has no unvisited neighbours - back up to A. Stack: [A]. |
| 18 | 1 | A has no branches left - pop it. The stack is empty. |
Good choice when…
- You only need to know if a path exists, or want everything reachable from a node - DFS gets there with a lean stack instead of BFS's wide frontier.
- You need cycle detection, topological sort, or strongly connected components - all three are DFS plus a little bookkeeping.
- Backtracking search: mazes, sudoku, n-queens. "Try a move, recurse, undo" is DFS over the graph of game states.
- Maze generation: a randomized DFS that carves corridors produces the classic long, winding mazes.
Bad choice when…
- You need the shortest path in an unweighted graph. That is BFS's superpower - DFS returns whatever path it stumbles into first.
- The graph can be very deep and you wrote the recursive version - a 100k-node chain means 100k stack frames. Use the iterative form.
- You want nodes level by level (nearest first, k-hop neighbours). DFS order jumps around; BFS order is sorted by distance.
Common mistakes
- Recursive DFS overflows the call stack on deep graphs - in most JavaScript engines somewhere around 10k frames. A path-shaped graph (a long chain, one maze corridor) is enough to crash it. Production DFS on unknown input should be iterative.
- The iterative version visits neighbours in reverse order compared to the recursive one, because a stack flips what you push. Same nodes visited, different order - push the neighbour list reversed if the order must match.
- For cycle detection in a directed graph, one `visited` set is wrong: it mixes up "on my current path" with "finished long ago", and only an edge back to the current path proves a cycle. When you mark matters too - gray goes on when you enter a node, black only when you leave it.
- DFS does not find shortest paths, but people reach for it out of habit. The first path it finds depends on neighbour order and can be absurdly long. "Fewest steps" questions are BFS questions, every time.
Depth-First Search vs. its closest relatives
| Algorithm | Best | Average | Worst | Space | Stable |
|---|---|---|---|---|---|
| Depth-First Searchthis page | O(V + E) | O(V + E) | O(V + E) | O(V) | - |
| Breadth-First Search | O(V + E) | O(V + E) | O(V + E) | O(V) | - |
| Topological Sort | O(V + E) | O(V + E) | O(V + E) | O(V) | - |
| Tree Traversals | O(n) | O(n) | O(n) | O(h) | - |