Depth-First Search: Exploring Deep Before Wide

Sep 4, 2024· 13 min read
Depth-First Search: Exploring Deep Before Wide
type
status
date
summary
tags
category
icon
password
featured
freq
difficulty
Depth-First Search (DFS) is an algorithm for traversing or searching tree or graph/matrix data structures. The algorithm starts at the root (or an arbitrary node) and explores as far as possible along each branch before backtracking. It's naturally implemented with recursion (system call stack) or with your own explicit stack.

The four canonical DFS styles

1. Pure Return-Value DFS (Recursive)

A postorder style where each call returns a value computed from the results of its required subcalls (i.e., its dependencies). No external mutable state is modified.
  • Idea: For a state S, first obtain the results of all dependent subproblems D₁ ... Dₖ via recursive calls, then combine them to produce and return result(S).
  • Characteristics
    • Often allocates intermediate objects during combination (e.g., list concatenation), which can be slower/more memory-intensive in Python.
    • Naturally expresses postorder evaluation: work happens on return after subcalls finish.
    •  
 

2. Side-effect Backtracking (Recursive)

This method passes state information (like the path prefix or accumulated sum) from the parent to the children. The core results are typically generated by updating a shared/global variable.
  • Idea: The function's parameters carry the "prefix" information.
  • Characteristic: Efficient as it avoids massive object creation; results are typically collected via a list or dictionary passed by reference, or by updating a global variable.
  • Application: Tree Traversal, Level Averages, and problems requiring the root-to-current-node path value.
    • It is ideal for search problems involving pruning (early exit from unpromising branches), managing streaming results, and carefully controlling state changes—often requiring logic for "same-level de-dup."
 
Backtracking template (choose → recurse → undo)
Example: Same-level de-dup (LeetCode 90 Subsets II)
 

3. Generator DFS (Recursive, streaming)

For scenarios involving massive search spaces or the need for early stopping without generating all results at once, using Generators that yield results is highly effective.
Pros: Tiny peak memory (besides current path); can early-exit.
Cons: Consumers must iterate; collecting to a list forfeits the memory advantage.
 

4. DFS with an Explicit Stack (Iterative)

Same search order as recursive DFS, but you manage the stack.
This implementation would help void recursion limits and has deterministic control over order.
 

Comparison

Style
Best For
Strengths
Trade-offs
Pure Returns
Tree DP, combining child results (heights, sizes, mins/maxes)
- Stateless recursion - Clean aggregation of results - Natural fit for trees
- Harder to track global state - Less flexible for path construction
Backtracking
Constraints, pruning, path building, prefix sums, streaming
- Fine-grained control over recursion - Easy to prune invalid paths - Good for stateful exploration
- Can be inefficient without pruning - Risk of recomputation without memoization
Generators
Massive search spaces, early stopping, lazy evaluation
- Memory-efficient - Supports streaming - Enables short-circuiting
- Harder to debug - May obscure control flow
Iterative
Deep recursion risk, team preference, system integration
- Avoids stack overflow - Explicit control over state - Easier to integrate with loops
- Verbose for recursive logic - Manual stack management can be error-prone

DFS in Tree Structures

In tree structures, Depth-First Search (DFS) explores each branch as deeply as possible before backtracking, naturally fitting recursive or stack-based traversal. It’s commonly used for tasks like traversal, collections (e.g., all paths, node lists) and aggregations (e.g., height, sums, lowest common ancestor) in hierarchical data.
notion image
 
DFS runs in O(N) time on a tree, where N is the number of nodes, since each node is visited exactly once. And the space complexity is O(H) for the recursion (or stack) depth, where H is the tree’s height.

Traversal

Bottom-up (return merged list)
Return and merge the recursive results of left and right subtrees.
Top-down (fill a list as you go)
Pass and update a shared list while traversing.

Path Builders

Prefer Bottom-Up When the result depends on child existence (e.g., path_to, all_paths).
Prefer Top-Down When we need pruning or early stopping using prefix info.
Goal
Bottom-Up Strategy
Top-Down Strategy
Notes
Path to a target node
Return the suffix path from the child when found, and prepend the current node while backtracking.
Pass a running prefix path downward and backtrack (append / pop).
Bottom-up is simpler since each call either returns None or a full path.
All root-to-leaf paths
Leaf returns [[root.val]]; parent prepends its own value to each child path and merges.
Use backtracking to track the path prefix and take snapshots at leaves.
Usually bottom-up is cleaner for static path enumeration.
Path to target
All root-to-leaf paths

Metrics & Aggregation

Goal
Bottom-Up Formula / Logic
Key Base Case / Note
Tree height
1 + max(left_height, right_height)
Return -1 for empty tree (height as edge count) or 0 if counting nodes.
Tree minimum value
min(root.val, left_min, right_min)
Return +inf for empty subtree.
Maximum root-to-leaf path sum
root.val + max(left_sum, right_sum)
Return root.val directly at a leaf; return -inf for empty branch.
Lowest Common Ancestor (LCA)
Recurse left/right; if both non-null, current node is LCA. Otherwise return non-null child.
Works for general binary trees, not just BSTs.
Tree height
Min value
Max root→leaf sum
Lowest Common Ancestor
 

DFS in Graphs / Matrices

Depth-First Search (DFS) explores as far as possible along each branch before backtracking, making it ideal for tasks like connected component detection, cycle checking, and pathfinding in graphs or matrices. In grid-based problems, DFS can be implemented recursively or iteratively to traverse adjacent cells, often using visited flags or boundary checks to avoid revisiting.
notion image
In general Graphs or Matrices (which can be viewed as grids/graphs), the use of a visited set is crucial to avoid infinite loops (cycles) and redundant work. Specifically, creates a cycle. Without tracking visited nodes, DFS would endlessly loop between A and B. And if multiple paths lead to the same node (e.g., A→C and B → C), visited ensures node C and its subtree are explored only once.
Its performance is highly efficient on graphs, achieving a time complexity of O(V + E) for visiting all vertices (V) and edges (E) once. And it requires space complexity of O(V) for the stack and the required visited structure;

General Graph DFS Template

 

Graph Application Examples

Goal
Method
Core Logic
Connected Components
Top-Down
Outer loop iterates all nodes. DFS (explore) simply marks all reachable nodes as visited. Outer loop increments a counter for each successful explore.
Component Size
Bottom-Up
DFS (explore_size) returns a numerical size. size = 1 + sum(explore_size(neighbor))
Island Count (Matrix)
Top-Down
Outer loop iterates grid cells. DFS marks connected land cells (L) as visited and returns True if a new island is found.
Example: Reachability between nodes
 
Example: Largest Component Size
 

Directed Graph Cycle Detection (Three-Color Marking)

To detect cycles in directed graphs, we need more than a simple visited set.
We use the Three-Color Marking technique to track nodes within the current recursion path.
Color
State (Set)
Purpose
White
Not in any set.
Unvisited.
Grey
visiting
Currently in the recursion stack (part of the current path).
Black
visited
Fully explored (all descendants processed).
Cycle Condition: If the search encounters a Grey node, a cycle exists.
 

Endnote

Depth-First Search (DFS) is a power tool for deep exploration and fundamental traversal strategy that explores as far as possible along each branch before backtracking. It can be implemented recursively or iteratively with an explicit stack, making it flexible for trees, graphs, and matrices.
上一篇
Implementing Efficient Prefix Search with Tries
下一篇
Breadth-First Search: Level-Order Exploration