07 / 20 · Tier B
Patterns / 07

DFS — depth-first search

Go deep before going wide. Recursion or an explicit stack; visited set to avoid cycles. DFS is the right tool when the question is "does any path exist", "find all connected components", or "is this graph acyclic". The three-colour variant catches cycles in directed graphs in a single pass, and is the trick most candidates remember halfway through writing the wrong version.


1 · Why DFS matters: the intuition

Depth-first search is the algorithmic encoding of "explore one branch fully before backing up to try the next". On a graph with V nodes and E edges, it visits every node and traverses every edge in O(V + E) time and O(V) space (the recursion stack plus the visited set). That's optimal for "exhaustive but unordered" exploration — the moment you don't care about path lengths, DFS is the cheapest tool that touches everything at least once.

The recursive/stack-based duality is the conceptual core. Recursive DFS uses the call stack to remember "what to do after this subtree". Iterative DFS makes that stack explicit — usually a slice or array of "work items". The two are operationally equivalent; you choose recursive for clarity (the code reads top-down like the problem) or iterative for control (when the input might blow the language's default recursion limit, which in Python is 1,000 and in many other languages is in the tens of thousands).

DFS is also the right shape when the problem says "any" rather than "shortest". "Find any path from A to B": DFS returns the first one it finds. "Find all configurations": DFS with backtracking enumerates them. "Is the graph acyclic": DFS detects a back-edge in one pass. None of these have an ordering constraint that would require BFS's layer-by-layer discipline.

The intuition in one line. DFS asks "can I get there at all", "are there any paths", or "does this graph have property X". BFS asks "what's the shortest", "how many steps", or "what does layer k look like". If the question doesn't mention shortest / fewest / minimum-distance, default to DFS. It's shorter to write and uses less memory on broad graphs.

2 · How to recognise it: six distinct shapes

  • "Does a path exist" / "is X reachable from Y". The classic reachability question. DFS gives a yes/no with no extra work.
  • Connected components. Loop over all nodes; DFS from each unvisited one; increment a counter. The cleanest connected-components pattern.
  • Cycle detection. Especially in directed graphs. Three-colour DFS detects back-edges directly.
  • Tree traversals. Pre/in/post-order. Each is a one-line variation on the same recursive shape.
  • "All paths" or "all configurations". When you need to enumerate (not just check), DFS with backtracking is usually cleaner than BFS.
  • Implicit graph traversal. Grid problems where you want to fill / flood. DFS via recursion is often shorter than BFS via a queue.

Shape A: connected components

"How many separate islands / regions / components are there?" Outer loop iterates every node; whenever you find one that's unvisited, DFS-flood from it and increment a counter. The set of nodes discovered during one DFS call is one component. Examples: LC 200 Number of Islands, LC 547 Number of Provinces, LC 695 Max Area of Island.

Shape B: cycle detection (different mechanics for directed vs undirected)

Undirected: a back-edge to anyone other than the parent is a cycle. Pass parent down the recursion; ignore the edge back to the parent. Two-colour visited set is sufficient.
Directed: a two-colour scheme can't tell back-edges from cross-edges. Use three-colour: WHITE (unseen) → GREY (on the current recursion stack) → BLACK (fully explored). A GREY hit is a cycle. The classic example is course-schedule cycle detection (LC 207).

Shape C: path between two nodes (or all paths)

"Does any path exist from A to B?": DFS from A, return true on finding B. "Find any one path": same DFS, accumulate the path on the stack frame, return when found. "Find all paths": DFS with backtracking; on success, copy the path into the result; on the way back, pop the last node. Examples: LC 797 All Paths From Source to Target, LC 1971 Find if Path Exists in Graph.

Shape D: island / region counting on a grid

A grid is an implicit graph: every cell is a node; neighbours are the four (or eight) adjacent cells. Loop over the grid; for each unvisited land cell, DFS-flood and mark visited. The number of DFS calls that found new land = the number of islands. Mutating the grid in-place ('1' → '0') is a clean way to mark visited without an extra set. LC 200 is the canon; LC 463 (Island Perimeter), LC 1254 (Closed Islands), LC 1568 (Minimum Days to Disconnect Island) are variations.

Shape E: topological sort via post-order

On a DAG, the reverse post-order of DFS gives a topological ordering. Run DFS from every unvisited node; on the way out of the recursion (after recursing into all children), push the node onto a stack. Reverse the stack at the end. This works because in post-order, a node is recorded only after all its descendants — ensuring it appears before none of its descendants in the reverse. Examples: LC 210 Course Schedule II, LC 269 Alien Dictionary.

Shape F: backtracking-flavoured DFS (with state)

DFS where you carry a partial solution down the recursion, extend it on entry, and undo the extension on the way back up. The "undo" step is what distinguishes backtracking from plain DFS. Used for enumeration: permutations (LC 46), combinations (LC 77), word search (LC 79), N-Queens (LC 51), Sudoku solver (LC 37). The mark/unmark on the visited set (or the constraint state) is the signature.

BFS vs DFS, refined. If the question contains "shortest" or "fewest", reach for BFS. If it contains "any", "all paths", "exists", or "cycle", reach for DFS. The exception: on tightly nested grids both work; pick whichever you find clearer (DFS via recursion is usually shorter for grids; BFS via queue is shorter for level-order tasks). On a directed graph with cycle-detection, DFS wins by a wide margin. Three-colour DFS does it in one pass, no BFS analogue is as clean.

3 · Sister algorithms

DFS sits inside a small family of "exhaustive search" algorithms. Each cousin trades a different axis: IDDFS trades repeated work for memory; bidirectional search trades simplicity for asymptotic speedup; randomised DFS trades determinism for diversity. Knowing the family prevents reaching for plain DFS when the constraint rules it out.

  • Iterative Deepening DFS (IDDFS). Run DFS with a depth limit of 1, then 2, then 3, and so on, until the goal is found. Wastes repeated work (the limit-3 call re-explores everything the limit-2 call did) but uses only O(depth) memory. Use it when (a) the branching factor is large enough that BFS's frontier blows memory, and (b) the depth of the answer is shallow relative to the depth of the tree. Standard in chess engines (iterative deepening with alpha-beta search), in AI route-finding with bounded budget, and in any "find any solution under a depth limit" task.
  • Bidirectional search. When you have both a start and a goal, search from both ends simultaneously, alternating layers. The two frontiers meet in the middle. Worst case O(b^(d/2)) instead of O(b^d): a square-root speedup. Standard for word-ladder problems (LC 127 BFS variant), and used in maps routing where you can search forward from origin and backward from destination. Tricky to implement correctly (synchronising the two frontiers, detecting the meeting point, reconstructing the path through both halves).
  • Randomised DFS for maze generation. A DFS where, at each node, you visit the neighbours in random order. Produces a spanning tree of the graph (a maze with no cycles). The classic algorithm behind procedurally-generated dungeon layouts in roguelikes. Variants (Wilson's algorithm, recursive division) trade different statistical properties of the resulting maze.
  • Tarjan's strongly connected components. A single-pass DFS that identifies all SCCs of a directed graph in O(V + E) using "low-link" values. Generalises three-colour DFS — instead of just detecting cycles, it groups every node into its SCC. Used by compilers (cyclic type definitions), build systems (cyclic dependencies), and link analysis (PageRank's reducible-vs-irreducible component handling).
  • Hierholzer's algorithm for Eulerian paths. A DFS that traverses every edge exactly once, when one exists (each vertex has even degree in the undirected case; in-degree = out-degree in the directed case). Used in LC 332 Reconstruct Itinerary. The trick: post-order accumulation of the path, then reverse — a DFS-with-stack idiom specifically tuned to edge traversal.
  • DFS-with-memoisation = top-down dynamic programming. The boundary between DFS and DP is just the memoisation cache. The moment you cache subproblem answers in a hash map keyed on "state at this node", DFS becomes DP. LC 329 Longest Increasing Path in a Matrix is the cleanest example: written as DFS with a cache, it's also DP.
The composition rule. DFS is rarely used alone in production. It's almost always combined with something: memoisation (DP), backtracking constraints (SAT solvers), bidirectional expansion (graph search), or randomisation (Monte Carlo Tree Search). Knowing the family helps you recognise when "DFS + X" is the right tool. And when X already has its own name (DP, SAT, MCTS), reaching for the named version is the senior signal.

4 · Canonical example: Course Schedule (LC 207)

Problem. Given numCourses and a list of prerequisite pairs [a, b] meaning "must take b before a", return whether it's possible to finish all courses. Equivalent to: is the directed prerequisite graph acyclic?

Input:  numCourses = 4, prerequisites = [[1,0],[2,1],[3,2]]
Output: true   (linear chain, no cycle)

Input:  numCourses = 2, prerequisites = [[1,0],[0,1]]
Output: false  (cycle: 0 → 1 → 0)

Build the adjacency list, then DFS from every unvisited node, marking nodes as IN-PROGRESS while recursing and DONE on return. A back-edge (an edge to a node currently in IN-PROGRESS) is a cycle. This is the three-colour idiom: WHITE = unseen, GREY = on the current recursion stack, BLACK = fully explored.

5 · Reference implementation

def can_finish(num_courses: int, prerequisites: list[list[int]]) -> bool:
    # Adjacency: course → list of courses that depend on it
    adj = [[] for _ in range(num_courses)]
    for a, b in prerequisites:
        adj[b].append(a)

    WHITE, GREY, BLACK = 0, 1, 2
    color = [WHITE] * num_courses

    def dfs(u: int) -> bool:
        if color[u] == GREY:
            return False               # back-edge → cycle
        if color[u] == BLACK:
            return True                # already verified acyclic
        color[u] = GREY
        for v in adj[u]:
            if not dfs(v):
                return False
        color[u] = BLACK
        return True

    return all(dfs(i) for i in range(num_courses))
Why three colours, not two. The naive "visited / not" scheme can't tell a back-edge from a cross-edge. WHITE → GREY → BLACK distinguishes "currently on the stack" (GREY) from "fully explored" (BLACK). A GREY hit means cycle; a BLACK hit means already-checked subtree. Two-colour visited mis-reports cross-edges as cycles in directed graphs.

6 · Variations

VariationWhat changesExample
Iterative DFSExplicit stack instead of recursion. Necessary in languages with shallow stacks (Python default ~1000) or for large graphs.Any recursive DFS, ported to a stack
Connected componentsOuter loop over nodes, DFS each unvisited one, increment counter.LC 200 Number of Islands (DFS variant)
Three-colour cycle detectionWHITE/GREY/BLACK; GREY-hit means back-edge.LC 207 Course Schedule, LC 802 Find Eventual Safe States
DFS with backtrackingMark on entry, undo on exit. Used for enumeration, path-finding-all, N-Queens.LC 79 Word Search, LC 51 N-Queens
Topological ordering (post-order)Push to result on the way out of the recursion; reverse at the end.LC 210 Course Schedule II
Tree DFS (pre/in/post)Visit before children / between / after. The order is the only thing that changes.LC 94, 144, 145

7 · Five-problem progression

#LCProblemWhy it's here
1LC 200Number of Islands (DFS)Same as the BFS version. Compare and see how recursion is shorter for grids.
2LC 207Course ScheduleCycle detection on a directed graph. The three-colour trick.
3LC 417Pacific Atlantic Water FlowMulti-source DFS from each ocean. Intersect the reachable sets.
4LC 79Word SearchDFS with backtracking on a grid. Mark visited, recurse, unmark on return.
5LC 802Find Eventual Safe StatesThree-colour DFS applied to "all paths lead to a terminal". Mid-difficulty trick.

8 · Three more worked problems

Three problems chosen to exercise three different DFS shapes from Section 2. The reference implementation above (LC 207) covered the three-colour idiom; these add island counting on a grid (Shape D) and the Eulerian-path post-order trick (Shape E with a Hierholzer twist).

8a · LC 207: Course Schedule, revisited with the 3-state contract

A second pass at LC 207 to make the three-colour contract precise. The reference implementation above used Python; here is the Go version with explicit colour transitions and the back-edge test written as a return value, so the propagation rule is unambiguous.

func canFinish(numCourses int, prerequisites [][]int) bool {
    adj := make([][]int, numCourses)
    for _, p := range prerequisites {
        adj[p[1]] = append(adj[p[1]], p[0])
    }

    const (
        WHITE = 0 // unvisited
        GREY  = 1 // on the current DFS stack (in progress)
        BLACK = 2 // fully explored
    )
    color := make([]int, numCourses)

    var dfs func(u int) bool
    dfs = func(u int) bool {
        if color[u] == GREY {
            return false // back-edge -> cycle
        }
        if color[u] == BLACK {
            return true // already verified acyclic
        }
        color[u] = GREY
        for _, v := range adj[u] {
            if !dfs(v) {
                return false
            }
        }
        color[u] = BLACK
        return true
    }

    for i := 0; i < numCourses; i++ {
        if !dfs(i) {
            return false
        }
    }
    return true
}
Why two colours fail on directed graphs. In an undirected graph, the only edges DFS doesn't traverse are back-edges (meaning the destination is an ancestor). In a directed graph, you can also have cross-edges (destination already finished, in a different subtree). A two-colour visited set can't distinguish them; both look like "I've seen this node before". Three-colour does: cross-edges target BLACK, back-edges target GREY. The bug signature is "reports a cycle that isn't there": your code thinks a cross-edge is a back-edge.

8b · LC 200: Number of Islands

Problem. Given an m × n 2D grid of '1's (land) and '0's (water), return the number of islands. An island is surrounded by water and formed by connecting adjacent lands horizontally or vertically.

Input:  [
  ['1','1','0','0'],
  ['1','1','0','0'],
  ['0','0','1','1'],
  ['0','0','1','1'],
]
Output: 2

Shape D in its purest form. Loop over every cell; whenever you find an unvisited '1', DFS-flood from it (mark all connected '1's as visited) and increment the island count. Mutating the grid in place ('1' → '0') is the cleanest way to mark visited without an extra set. But say "I'm mutating the input" out loud in an interview before doing so.

func numIslands(grid [][]byte) int {
    if len(grid) == 0 {
        return 0
    }
    m, n := len(grid), len(grid[0])
    count := 0

    var dfs func(r, c int)
    dfs = func(r, c int) {
        if r < 0 || r >= m || c < 0 || c >= n || grid[r][c] != '1' {
            return
        }
        grid[r][c] = '0' // mark visited by sinking
        dfs(r-1, c)
        dfs(r+1, c)
        dfs(r, c-1)
        dfs(r, c+1)
    }

    for r := 0; r < m; r++ {
        for c := 0; c < n; c++ {
            if grid[r][c] == '1' {
                count++
                dfs(r, c)
            }
        }
    }
    return count
}
The four-directions idiom. Almost every grid DFS uses the same neighbour pattern: up/down/left/right. Some problems use eight-directional (add the diagonals). The bounds check (r < 0 || etc.) at the top of the recursive function is cleaner than inside each of the four calls — it consolidates the "don't fall off the edge" logic in one place.

8c · LC 332 (HARD): Reconstruct Itinerary (Hierholzer's Eulerian path)

Problem. Given a list of airline tickets where tickets[i] = [from, to], reconstruct the itinerary in order. All tickets belong to a man who departs from "JFK"; the itinerary must use all tickets once. If there are multiple valid itineraries, return the lexicographically smallest.

Input: tickets = [["MUC","LHR"],["JFK","MUC"],["SFO","SJC"],["LHR","SFO"]]
Output: ["JFK","MUC","LHR","SFO","SJC"]

This is an Eulerian path problem: traverse every edge exactly once, starting at JFK. Hierholzer's trick: post-order DFS, accumulating the path on the way back. Then reverse the result at the end. Sort each node's adjacency list so the lexicographically smallest neighbour is always tried first.

import "sort"

func findItinerary(tickets [][]string) []string {
    // Build adjacency, each list sorted descending so we can pop() the smallest
    adj := map[string][]string{}
    for _, t := range tickets {
        adj[t[0]] = append(adj[t[0]], t[1])
    }
    for k := range adj {
        sort.Sort(sort.Reverse(sort.StringSlice(adj[k])))
    }

    var route []string
    var dfs func(node string)
    dfs = func(node string) {
        for len(adj[node]) > 0 {
            // Pop the lexicographically smallest neighbour (end of reverse-sorted list)
            n := len(adj[node])
            next := adj[node][n-1]
            adj[node] = adj[node][:n-1]
            dfs(next)
        }
        route = append(route, node) // POST-ORDER: add on the way out
    }

    dfs("JFK")

    // Reverse the route
    for i, j := 0, len(route)-1; i < j; i, j = i+1, j-1 {
        route[i], route[j] = route[j], route[i]
    }
    return route
}
Why post-order plus reverse, not pre-order. The naive pre-order approach (record the node on entry) breaks when the greedy lex-smallest choice leads to a dead-end before all edges are used. Post-order accumulation gracefully handles this: the dead-end node gets added first, then its predecessors, then its predecessor's predecessors. Reversing at the end gives the correct Eulerian traversal. This is exactly the same idea as DFS-based topological sort — post-order produces the reverse of the topological order, so you reverse to get the right answer.

9 · How to solve hard DFS problems, step-by-step

DFS rewards a few minutes of upfront thinking. Four questions to answer before writing the recursive function — they determine the signature, the base case, and whether you need backtracking.

Step 1: What state does the recursion carry?

Every recursive DFS function has a signature; the signature is the state. For "is X reachable", the only state is the current node. For "find any path from A to B", you carry the path-so-far. For N-Queens, you carry the partial board (or just the columns and diagonals used). The signature design decision is "what does the caller need to pass in for this call to make sense?". Get this wrong and you'll end up using global variables, which leak between calls and make backtracking impossible.

Step 2: What's the base case?

Three options: (a) you've found the answer (success: return / record). (b) You've hit a dead-end (failure: return without recording). (c) You've already visited this node (avoid revisit: return). Often two or three of these apply. Write them out as if statements at the top of the function before the recursion. The base case is where the recursion terminates; without it, you'll stack-overflow on the first cycle.

Step 3: Pure exploration or backtracking?

Pure exploration: mark visited and never look back. Used when "have I been here" is the only question (LC 200 Number of Islands, LC 207 Course Schedule). The visited set is global to the whole search.

Backtracking: mark on entry, recurse, unmark on exit. Used when you need to enumerate multiple paths through overlapping state space (LC 79 Word Search, LC 51 N-Queens). The visited set is effectively "the path so far"; on the way back up, you remove the current node so other paths can explore through it.

The distinguishing rule: do other valid solutions pass through this node? If yes, you need backtracking. If no, plain exploration suffices.

Step 4: Iterative or recursive?

Default to recursive; it reads top-down like the problem and is shorter. Switch to iterative (explicit stack) when:

  • The graph depth can exceed your language's recursion limit (Python: ~1,000; Go: ~250k effective in most builds, but be careful with goroutines whose stacks start small).
  • You need fine-grained control over the stack (e.g. early-termination from deep in the recursion without unwinding via exceptions).
  • You're translating to a language without good tail-call optimisation, and the recursion is tail-recursive.

Iterative DFS uses a []Node slice as the stack: append to push, slice [:n-1] to pop. The only subtlety: when you push neighbours, push them in reverse order if you want them processed in the natural left-to-right order (because the stack pops in LIFO order).

The four-question checklist. State (the function signature). Base case (when does recursion stop). Pure-or-backtrack (do you unmark on the way back). Iterative-or-recursive (will the depth blow the stack). Five minutes of writing answers to these in a comment block before coding saves 30 minutes of debugging.

10 · DFS exploration order on a small graph

A six-node directed graph, traversed by DFS starting at A. The numbers next to each node are the discovery time (when the node was coloured GREY); the order in which BLACK is assigned is the post-order.

The discovery times (d=1, d=2, ...) record when each node first turned GREY; that's the pre-order. The reverse of the order in which nodes finish (turn BLACK) is the topological sort — the classic way to compute it from DFS. Here, post-order is F, D, B, E, C, A; reversed, that's A, C, E, B, D, F, a valid topological ordering of the DAG.

11 · Common pitfalls: recursion depth, visited, mutating graphs

  • Recursion depth blowing up. Python's default recursion limit is ~1000. A 10⁴-node graph in a linear chain crashes. Either raise the limit (sys.setrecursionlimit) or port to an iterative DFS.
  • Forgetting to mark visited. On a cyclic graph, DFS loops forever. Even on a DAG, you'll re-explore subtrees exponentially.
  • Two-colour cycle detection on a directed graph. Mis-reports cross-edges as cycles. Always use three-colour for directed graphs.
  • Mutating the input. Tempting on grid problems ("set visited = '#'"), but destroys the input. Interviewers usually want non-destructive.
  • Backtracking that forgets to undo. Mark on entry, unmark on exit. If you mark globally without undoing, you can't enumerate multiple paths.
  • Building the result list inside the recursion. Pass the result by reference; don't return a list per call and try to concatenate. The copy cost is O(V) per node.
  • Counting paths twice. Bidirectional edges in an undirected graph: track the parent to avoid going back the way you came.
  • Recursion depth limit, in detail. Python's default is 1,000, easy to blow on a linked-list-as-tree or a deeply nested JSON. Go's effective limit is around 250,000 frames for the default stack (Go grows the stack dynamically, but you'll hit a memory or scheduler limit eventually). Java/Go default thread stacks handle ~10,000 frames before a typical SO. Rust on a 2 MB thread stack: ~50,000. If the input has any chance of exceeding 10⁴ depth, convert to an explicit stack. Recursive DFS will simply crash.
  • Not marking visited causes infinite loop. The single most common DFS bug on cyclic graphs. The visited set must be checked at the top of the recursive function, before recursing. A correct shape: if visited[u] { return }; visited[u] = true; for v in adj[u] { dfs(v) }. Forgetting either the check or the mark, and you'll either loop forever or revisit subtrees exponentially.
  • Modifying the graph during DFS. Tempting in problems that ask to "remove edges as they're traversed". But the recursion is currently inspecting adj[u]; mutating it under your feet via adj[u] = adj[u][:0] or similar can skip nodes or process them twice. The safe pattern: snapshot the adjacency list at the top of the call, iterate the snapshot, modify in a separate pass. The unsafe pattern triggers the dreaded "the bug only appears on inputs of size > 50" class of failure.
  • Not restoring state in backtracking flavour. The backtracking step (mark on entry / unmark on exit) is symmetric. If you unmark in the wrong place (e.g. inside the for-loop instead of after), you're either restoring state too early (corrupting the current path) or too late (corrupting siblings' state). Rule: the mark and unmark live at the very start and very end of the recursive function, with nothing else between them at the same nesting level. The unmark is the LAST statement before returning.
  • Adjacency list built backwards. Especially in cycle-detection problems with prerequisites: "a depends on b" can be encoded as adj[a] = [b] (a points to its dependencies) or adj[b] = [a] (b points to its dependents). Both are valid graphs, but DFS over one is not equivalent to DFS over the other. The cycle direction flips. Decide which encoding you want and stick with it; misencoding is silent.
  • Iterative DFS that processes nodes in the wrong order. A stack pops in LIFO; if you want children processed left-to-right, you push them right-to-left. Forgetting to reverse-push gives mirror-image output. Doesn't matter for correctness of reachability or component-count, matters for any problem where the discovery order is the answer.

12 · Complexity, side by side

DFS and BFS share the same asymptotic time complexity, but their space and traversal properties diverge in ways that decide which one fits the problem.

AspectDFSBFS
TimeO(V + E)O(V + E)
SpaceO(h) where h = max depth — call stack + visitedO(w) where w = max width of the frontier
Shortest path (unweighted)No — finds a path, not the shortestYes — first reach = shortest reach
Cycle detection (directed)Yes — three-colour DFS, O(V + E)Possible but awkward
Topological sortYes — reverse post-orderYes — Kahn's algorithm
Pathological caseDeep linear chain — recursion overflow at ~10k framesWide graph — frontier can exceed RAM
Cache localityBetter — depth-first goes deep on one subtree at a timeWorse — fans out across nodes
Memory patternOne subtree fully, then nextLayer-by-layer breadth scan
The non-obvious rule. Use DFS unless the problem asks for "shortest" or "minimum steps." Then it's BFS. DFS is cheaper in space when the graph is broad and shallow; BFS is cheaper when the graph is deep and narrow. The classic example: a tree with branching factor 10 and depth 1,000: BFS frontier hits 10⁵, DFS stack hits 10³.

13 · Traversal orders, in one diagram

The same DFS visit can produce three different orderings depending on when you record the node. Each is the right tool for a different class of problem.

# Three orderings, one traversal — only the print position changes.
def dfs(node):
    if node is None: return
    print(node)        # PRE-order
    dfs(node.left)
    # print(node)      # IN-order
    dfs(node.right)
    # print(node)      # POST-order

Pre-order serialises a tree: root, then recursively each subtree — used by JSON serialisation, tree copying, and prefix expression printing. In-order on a BST emits the elements sorted; this is the bridge between binary search trees and ordered iteration. Post-order processes children before parent — used to free a tree safely, evaluate an expression tree, and generate topological orderings.

14 · Real-world DFS, in production code

DFS isn't an algorithm interview question — it's the algorithm half the systems on your machine run constantly. Five places it shows up by name:

File-system traversal: find(1), du(1), git status.
The kernel's recursive directory walk is a textbook DFS, with the visited set replaced by inode-uniqueness. find . -type f is depth-first by default; reversing to BFS (find -depth) processes children before parent, which matters for rm -rf ordering.
Compiler dependency resolution.
npm, cargo, pip, go modules: every package resolver does a DFS over the dependency graph, detecting cycles with three-colour marking. The "cyclic dependency" error every developer has hit is the BLACK→GREY edge being caught.
HTML / XML parsers and the DOM.
Browser engines walk the DOM tree depth-first during layout and paint. document.querySelectorAll returns nodes in document order, which is a pre-order DFS. Removing a subtree from the DOM internally does a post-order walk to fire detachment events from the leaves up.
Web crawlers, in a controlled form.
Google's early crawlers were DFS, then BFS (better breadth, less hostile to sites), then a hybrid with priority. The visited set is a Bloom filter sized for billions of URLs.
Type-checkers and topological order.
TypeScript, Rust's borrow checker, Haskell's GHC: all do a post-order DFS over the type graph to type-check expressions bottom-up. The same machinery underlies build systems like Bazel and Make.
Stat worth knowing. Python's default recursion limit is 1,000, easy to blow on a linked-list-as-tree or a deeply nested JSON. Java/Go default thread stacks handle ~10,000 frames before stack overflow. Rust on a 2 MB thread stack: ~50,000. If the input has any chance of exceeding 10⁴ depth, convert to an explicit stack. Recursive DFS will simply crash.

15 · What to say in the interview

  1. "Reachability / components / cycle → DFS." Name the pattern. Specify directed vs undirected.
  2. "Recursion or explicit stack." Mention both. Recursion is shorter; iterative is needed for very deep graphs.
  3. "Visited set to avoid revisiting." Critical on cyclic graphs.
  4. "For directed cycle detection, three-colour DFS." Articulate WHITE/GREY/BLACK and why two-colour isn't enough.
  5. "Time O(V + E). Space O(V) for visited plus O(V) recursion stack." State both.
  6. Edge cases. Empty graph, single node, self-loops, disconnected components, very deep linear chains (recursion limit).

16 · Where this gets asked

Specific companies hit DFS-shaped questions over and over. Names and what they tend to ask, from public Leetcode and Glassdoor breakdowns plus team-blog mentions:

CompanyCommon framingWhy it fits their stack
GoogleCourse Schedule (LC 207), Word Search (LC 79), Number of Islands (LC 200)Map & Knowledge Graph traversal. Graph problems are the bread and butter of L4/L5 phone screens.
MetaClone Graph (LC 133), Pacific Atlantic Water Flow (LC 417), All Paths Source-Target (LC 797)Social graph at the core; DFS is the candidate's chance to show comfort with adjacency lists and visited sets.
AmazonWord Ladder (LC 127 — BFS variant), Number of Provinces (LC 547), Coloring a Border (LC 1034)Warehouse routing and product-graph teams use these patterns directly.
MicrosoftRobot Room Cleaner (LC 489), Surrounded Regions (LC 130), Word Break II (LC 140)Office and Windows teams still ask the classic graph-traversal canon.
Stripe / Datadog / CoinbaseCycle detection in a directed dependency graph, transaction chainsPayment processing builds DAGs of dependent operations; cycle detection lands in real reviews.
Uber / Lyft / DoorDashReachability on a road graph, time-bounded BFS/DFSRouting problems start with reachability before they reach shortest-path.
Pattern of patterns. Three sub-shapes recur: (1) "is X reachable from Y" (plain DFS + visited), (2) "find a cycle / detect a deadlock" (three-colour DFS), (3) "count connected components / islands" (DFS on a grid, every unvisited cell a new component). If you can solve those three from a blank page, you can solve any DFS interview question with adaptations.

17 · Try it yourself

Three problems, each one teaches a different DFS muscle. Spend 30 minutes on each before checking the hint — the struggle is where the pattern internalises.

Number of Islands · LC 200
The canonical grid-DFS. Iterate every cell; if it's land and unvisited, DFS-flood it and bump the count. The whole solution is 15 lines. Hint: a single visited set works, but mutating the grid in-place to '0' is cleaner.
Course Schedule · LC 207
The cycle-detection canon. Build the prereq graph, three-colour DFS, return false on any GREY→GREY edge. Hint: this is also doable via Kahn's algorithm (topological-sort BFS); doing it both ways teaches when each wins.
Word Search · LC 79
DFS with backtracking: your first time mixing the two. Pick a starting cell, walk the word letter-by-letter, undo the visit on the way back. Hint: the four-direction loop is the same in every grid problem; pre-define dirs = [(0,1),(0,-1),(1,0),(-1,0)] and reuse it.
Stretch: Critical Connections · LC 1192
Tarjan's bridge-finding algorithm. A serious DFS extension that introduces "low-link" — the smallest discovery time reachable from a subtree. Hint: the moment to attempt this is after you've done the three above blindfolded; it teaches the deepest DFS variant you'll see in interviews.
How to practise. First pass: write it from scratch, no LLM, no autocomplete. Second pass: explain it out loud to an imaginary interviewer while you write. Third pass: rewrite without re-reading your first attempt. By the third pass it's in your fingers, not your notes.

Further reading

  • CLRS — Chapter 22. The three-colour scheme is from here; the formal treatment is worth reading once.
  • Sedgewick & Wayne — Algorithms, Chapter 4. Cleaner code than CLRS; same content.
  • Adjacent: BFS. The other half of graph traversal.
  • Adjacent: Topological sort. Post-order DFS gives you topological order for free.
  • Adjacent: Backtracking. DFS plus mark/unmark, the enumeration pattern.
  • Semicolony: Algorithm visualiser. Step through DFS and watch the order of node visits.
Found this useful?