Skip to content
Build With Owais
Graph AlgorithmsIntermediate

Depth-First Search Visualizer

Follow one branch as deep as it goes, then backtrack — a stack, or recursion.

Owais Noor 13 min read Updated
XLinkedInWhatsApp

Interactive Depth-First Search visualiser

Depth-first search commits. It picks a direction, follows it as far as it goes, and only when it can go no further does it back up and try the road not taken. Where breadth-first search is cautious and systematic, DFS is the algorithm equivalent of walking into a maze and always turning left.

That single behaviour makes it the foundation of a surprising amount of computer science. Cycle detection, topological sorting, strongly connected components, articulation points, maze generation, and every backtracking solver you have ever written are all DFS with different bookkeeping attached. It is less a search algorithm than a way of imposing structure on a graph.

What is depth-first search?

DFS explores a graph by going as deep as possible along each branch before backtracking. From the current node it picks an unvisited neighbour and moves there immediately, repeating until it reaches a node with no unvisited neighbours — a dead end — at which point it retreats to the most recent node that still has options.

It can be written recursively, where the language's call stack does the remembering, or iteratively with an explicit stack. The two are equivalent; the recursive form is shorter and the iterative form is what you need when the graph is deep enough to overflow the call stack.

The visualiser uses the explicit-stack version deliberately. The recursive form's state lives in the call stack, which is invisible — you cannot show a learner a data structure they cannot see. With an explicit stack, the thing driving the traversal is on screen.

Why it works

DFS's correctness claim is weaker than BFS's and worth stating precisely: DFS visits every node reachable from the start, exactly once. It makes no claim about path length, and the path it finds to a node is frequently not the shortest one.

Completeness follows from the stack. Every discovered node is pushed, and the loop continues until the stack is empty, so nothing discovered is ever abandoned. The visited set guarantees each node is processed once, which also guarantees termination on cyclic graphs.

The property that makes DFS powerful is not about reachability at all — it is about timing. Each node has a discovery time (when DFS enters it) and a finish time (when DFS has exhausted its subtree and backs out). The nesting relationship between those intervals encodes the structure of the graph: if u's interval contains v's, then v is a descendant of u in the DFS tree. Topological sorting, cycle classification and strongly connected components are all read off these times rather than off the traversal order.

Step-by-step walkthrough

Same graph as the BFS example: A connects to B and C; B connects to D; C connects to D and E. Start at A, exploring neighbours in alphabetical order.

  1. Push A. Pop A and visit it. Push its neighbours: stack is [C, B] (B on top, so B goes first).
  2. Pop B and visit it. Push its unvisited neighbour D. Stack: [C, D].
  3. Pop D and visit it. Its neighbours B and C — B is visited, C is already on the stack. Stack: [C].
  4. Pop C and visit it. Push its unvisited neighbour E. Stack: [E].
  5. Pop E and visit it. No unvisited neighbours. Stack empty; traversal ends. Order: A, B, D, C, E.
  6. Compare with BFS on the same graph, which gave A, B, C, D, E. DFS reached D at step three, via A→B→D; BFS reached it last. Neither is wrong — they are answering different questions.

What DFS is really for

Using DFS to find a path between two nodes works, but it is the least interesting thing it does — and the path it returns may be far from shortest. The real uses come from the discovery and finish times.

Cycle detection. In a directed graph, an edge to a grey node — one currently on the recursion path — is a back edge and proves a cycle exists. This is how build systems detect circular dependencies and how type checkers catch recursive type definitions.

Topological sorting. Push each node onto a list when it *finishes*, then reverse the list. The result orders every node before all of its dependents. This is the algorithm behind build ordering, task scheduling with prerequisites, and spreadsheet recalculation.

Connected components. Run DFS from each unvisited node in turn; each run marks exactly one component. This is O(V + E) for the whole graph and is how flood fill, island counting and clustering problems are solved.

Backtracking. Sudoku solvers, n-queens, maze generation and constraint satisfaction are DFS over an implicit graph of partial solutions, where 'backtrack' means undoing the last decision. The graph is never built; it is explored as it is generated.

Strongly connected components via Tarjan's or Kosaraju's algorithm, and articulation points and bridges — the nodes and edges whose removal disconnects a network — are all DFS with extra bookkeeping on the timing information.

How the animation above works

Watch the stack panel rather than the graph. Nodes enter and leave from the same end, and the traversal repeatedly dives to a dead end before the stack shortens and a different branch begins. That saw-tooth pattern — deepen, deepen, deepen, retreat — is the signature of depth-first exploration.

Run BFS and DFS on the same graph from the same start and compare the visited-order panels. BFS's order fans out evenly; DFS's snakes. Then set a destination node: DFS will often find a path with noticeably more edges than BFS's, which is the clearest demonstration that DFS makes no shortest-path guarantee.

Time and space complexity

O(V + E) time with an adjacency list — identical to BFS, and for the same reason. Every vertex is processed once, and every edge is examined once from each endpoint. With an adjacency matrix it becomes O(V²).

O(V) space in the worst case for the visited set, plus the stack. The important practical difference from BFS is the *shape* of that stack usage: DFS's stack holds the current path, so it is O(depth), while BFS's queue holds an entire level, which can be O(V) on a branching graph.

On a wide, shallow graph DFS uses dramatically less memory than BFS. On a deep, narrow one — a long chain — DFS's stack reaches O(V) and the recursive form risks a stack overflow. This is the main reason to prefer the iterative version for graphs of unknown shape.

Recursive or iterative?

The recursive form is shorter, reads closer to the definition, and gives you finish times for free — the code after the recursive call runs exactly when the node finishes. For topological sorting and cycle detection this matters, because those algorithms are naturally expressed in terms of finish times.

The iterative form has no depth limit. Python defaults to a recursion limit around 1,000, and a graph of ten thousand chained nodes will crash a recursive DFS long before it runs out of memory. Java and JavaScript fail similarly at a few thousand frames.

One subtlety catches people out: the iterative version pushes neighbours in reverse order to match the recursive version's traversal sequence, because the last one pushed is the first one popped. Neither order is more correct, but if you are comparing outputs between implementations, this is usually why they differ.

Complexity at a glance

The numbers below are what the counters in the visualiser converge on. Raise the array size and watch the comparison count grow at the rate this table predicts.

Complexity and properties of Depth-First Search
Best-case timeO(V + E)
Average-case timeO(V + E)
Worst-case timeO(V + E)
Auxiliary spaceO(V)

Pseudo code

Language-agnostic, and close enough to the implementations below that you can read them side by side.

  1. 1procedure DFS_recursive(graph, node, visited)
  2. 2 add node to visited
  3. 3 process(node) // pre-order: on entry
  4. 4
  5. 5 for each neighbour of node do
  6. 6 if neighbour not in visited then
  7. 7 DFS_recursive(graph, neighbour, visited)
  8. 8 end if
  9. 9 end for
  10. 10
  11. 11 finish(node) // post-order: powers topological sort
  12. 12end procedure
  13. 13
  14. 14procedure DFS_iterative(graph, start)
  15. 15 visited := empty set
  16. 16 stack := empty LIFO stack
  17. 17 push start
  18. 18
  19. 19 while stack is not empty do
  20. 20 node := pop()
  21. 21 if node in visited then continue
  22. 22 add node to visited
  23. 23 process(node)
  24. 24
  25. 25 // Reversed, so the first neighbour is popped first.
  26. 26 for each neighbour of node in reverse order do
  27. 27 if neighbour not in visited then push neighbour
  28. 28 end for
  29. 29 end while
  30. 30end procedure

Depth-First Search in five languages

Working reference implementations — copy or download any of them. They are written to be read, so the comments explain the decisions rather than restating the code.

/**
 * DFS, both forms. They visit the same nodes; the iterative version pushes
 * neighbours reversed so the traversal ORDER matches the recursive one.
 */
function dfsRecursive(graph, node, visited = new Set(), order = []) {
  visited.add(node);
  order.push(node);

  for (const neighbour of graph.get(node) ?? []) {
    if (!visited.has(neighbour)) {
      dfsRecursive(graph, neighbour, visited, order);
    }
  }

  return order;
}

function dfsIterative(graph, start) {
  const visited = new Set();
  const order = [];
  const stack = [start];

  while (stack.length > 0) {
    const node = stack.pop();
    // A node can be pushed several times before it is popped, so re-check.
    if (visited.has(node)) continue;

    visited.add(node);
    order.push(node);

    const neighbours = [...(graph.get(node) ?? [])].reverse();
    for (const neighbour of neighbours) {
      if (!visited.has(neighbour)) stack.push(neighbour);
    }
  }

  return order;
}

const graph = new Map([
  ["A", ["B", "C"]],
  ["B", ["A", "D"]],
  ["C", ["A", "D", "E"]],
  ["D", ["B", "C"]],
  ["E", ["C"]],
]);

console.log(dfsRecursive(graph, "A")); // ["A", "B", "D", "C", "E"]
console.log(dfsIterative(graph, "A")); // ["A", "B", "D", "C", "E"]

Advantages and disadvantages

Advantages

  • O(depth) memory rather than O(width), which makes it far cheaper than BFS on wide, shallow graphs.
  • Its finish times encode graph structure, which is what makes topological sorting, cycle detection and strongly connected components possible.
  • The recursive form is short and reads almost exactly like the definition of the traversal.
  • The natural framework for backtracking problems — sudoku, n-queens, constraint satisfaction — where the graph is generated as it is explored.
  • Finds connected components in a single O(V + E) sweep over the whole graph.
  • Reaches deep targets quickly when the answer is far from the start, where BFS would first explore every shallower node.

Disadvantages

  • No shortest-path guarantee — the path it finds can be arbitrarily longer than the optimal one.
  • The recursive form can overflow the call stack on deep graphs; Python's default limit of about 1,000 frames is reached easily.
  • Not complete on infinite graphs: it can descend forever down one branch and never examine the others.
  • Its traversal order depends on the order neighbours are listed, which makes results implementation-dependent and tests brittle.
  • Cycle detection in directed graphs needs three colours rather than a simple visited set, which is a common source of subtle bugs.
  • O(V) stack in the worst case on a chain-shaped graph, giving away its usual memory advantage over BFS.

Real-world applications

  • Topological sorting for build systems, task schedulers, dependency resolution and spreadsheet recalculation.
  • Cycle detection — circular imports, circular dependencies in package managers, and deadlock detection in resource graphs.
  • Finding connected components, flood fill, island counting and image segmentation.
  • Backtracking solvers: sudoku, n-queens, maze generation, crossword filling and constraint satisfaction generally.
  • Tarjan's and Kosaraju's algorithms for strongly connected components, used in compiler optimisation and social network analysis.
  • Finding articulation points and bridges — the nodes and links whose failure would disconnect a network.
  • Tree and expression traversals: pre-order, in-order and post-order are all DFS with the processing step in different places.
  • Garbage collection mark phases, which trace reachable objects depth-first from the roots.

Interview questions

The questions that actually get asked about Depth-First Search, with the answers an interviewer is listening for.

  1. Q1. Why does DFS not find the shortest path?

    Because it commits to a branch and follows it to the end before considering alternatives. The first time it reaches a node is via whatever route it happened to take, not the shortest one — a node two edges away might be found after a twenty-edge detour. BFS gets the guarantee because it explores strictly in order of increasing distance; DFS explores in order of most-recently-discovered.

  2. Q2. How do you detect a cycle in a directed graph with DFS?

    Use three states rather than a visited flag: white for undiscovered, grey for nodes on the current recursion path, and black for fully finished nodes. An edge to a grey node is a back edge and proves a cycle. A visited set alone cannot distinguish that from a cross edge to an already-finished branch, which is harmless — that distinction is exactly what the grey state provides.

  3. Q3. How does DFS produce a topological sort?

    Append each node to a list when it *finishes* — after all its descendants are done — then reverse the list. A node finishes only once everything it points to has finished, so reversing the finish order puts every node before its dependents. The same traversal also detects cycles, which is important because a cyclic graph has no valid topological order at all.

  4. Q4. When would you choose iterative DFS over recursive?

    When the graph might be deep. Recursion depth is bounded by the language runtime — around 1,000 frames in Python by default, a few thousand in Java and JavaScript — so a long chain crashes a recursive implementation. The iterative version uses heap memory for its stack and is limited only by available memory. The trade is that finish times are less convenient to capture iteratively.

  5. Q5. What is the difference in memory usage between BFS and DFS?

    DFS's stack holds the current path, so it is O(depth). BFS's queue holds an entire level, so it is O(width), which on a branching graph can approach O(V). On a wide shallow graph DFS uses far less memory; on a long chain the two converge, since the path is the whole graph.

Common mistakes

  • Using a two-state visited set for directed cycle detection, which reports cycles where only harmless cross edges exist.
  • Failing to re-check visited after popping in the iterative version — a node can be pushed several times before it is first popped.
  • Marking nodes visited when pushing rather than when popping in the iterative form, which skips nodes that should be explored via a different branch in some variants.
  • Using recursive DFS on graphs of unknown depth and hitting a stack overflow in production on data that worked in testing.
  • Recording the topological order on entry rather than on finish, which produces an order that looks plausible and is wrong.
  • Forgetting that a full traversal of a disconnected graph needs an outer loop over all unvisited nodes, not one DFS call.
  • Expecting DFS to return a shortest path, which it makes no promise about.

Optimisation tips

  • Prefer the iterative form when the graph's depth is unknown or user-controlled — it removes an entire class of production failure.
  • For cycle detection and topological sort, keep the recursive form if depth allows: finish times fall out naturally from the code position after the loop.
  • Use an explicit colour array rather than two separate sets; it is one lookup instead of two and makes the three states obvious.
  • For grid problems, index cells into a flat array rather than hashing coordinate pairs — the constant-factor difference is substantial.
  • In backtracking, undo the state change immediately after the recursive call returns rather than copying state into each branch; copying is what makes naive backtracking slow.
  • Prune aggressively in backtracking searches — checking feasibility before recursing eliminates entire subtrees, and that dominates any micro-optimisation of the traversal itself.

Summary

Depth-first search follows one branch as far as it goes, then backtracks to the most recent node with unexplored options. It runs in O(V + E) time and uses O(depth) memory, which makes it much cheaper than BFS on wide graphs and riskier on deep ones, where the recursive form can overflow the call stack.

Its value is not path-finding — it offers no shortest-path guarantee — but structure. The discovery and finish times it produces are what make topological sorting, directed cycle detection, strongly connected components and articulation points possible, and every backtracking solver is DFS over a graph that is generated as it is explored. Learn the traversal, then learn what the finish times tell you; the second part is where the algorithm earns its place.

Frequently asked questions

DFS takes the most recently discovered node from its frontier (a stack), BFS takes the oldest (a queue). DFS dives deep and backtracks; BFS expands in rings. BFS finds shortest paths in unweighted graphs and DFS does not, but DFS uses O(depth) memory against BFS's O(width) and produces the finish-time information that topological sorting and cycle detection depend on.

Recursively when the graph is shallow and you need finish times — the code is shorter and the post-order step falls out naturally. Iteratively when depth is unknown or large, because recursion is bounded by the runtime's stack (about 1,000 frames in Python) while an explicit stack is bounded only by memory.

In a directed graph, by tracking three states: undiscovered, currently on the recursion path, and finished. Reaching a node that is on the current path means you have looped back onto yourself, which is a cycle. In an undirected graph it is simpler — an edge to any visited node that is not the immediate parent indicates a cycle.

Its stack only holds the current path from the start to the node being explored, which is O(depth). BFS's queue holds an entire level of the graph, which on a branching structure can be a large fraction of all vertices. On wide shallow graphs the difference is dramatic; on a long chain the two converge.

Most of its important uses are not searching at all. Topological sorting for build and dependency ordering, cycle detection for circular imports and deadlocks, connected components and flood fill, strongly connected components via Tarjan's algorithm, articulation points in network reliability, and every backtracking solver — sudoku, n-queens, constraint satisfaction — which is DFS over a graph generated on the fly.

Questions & discussion

Comments are not open yet — a hosted comment widget would cost every reader a chunk of JavaScript, and this playground is built to stay fast.

Spotted an error in the Depth-First Search explanation, or want a walkthrough of something that is not here? Tell me directly — I read everything and corrections get made the same week.

Start a project

Tell me what you're building

Share a few details and I'll come back within one business day with an honest take, a realistic timeline and a ballpark cost — no pressure, no sales script.

  • Reply within one business day, from me directly
  • Straight answers on scope, timeline and budget
  • Free 30-minute consultation, no obligation

Building something that needs this done properly?

Tell me what you're working on. I'll reply within one business day with honest, practical next steps.