SECTION 1
Historical Context & Motivation
Long before computers existed, mathematicians were fascinated by the problem of navigating complex structures like mazes and networks. The core question was deceptively simple: given a starting point and a web of connected paths, how can you guarantee you visit every reachable location exactly once? This is the fundamental problem that Depth-First Search (DFS) was designed to solve. DFS is one of the most important algorithms in all of computer science, forming the backbone of solutions ranging from maze generation to social-network analysis.
1735 • Euler and the Königsberg Bridges
1882 • Trémaux's Maze Algorithm
1945 • Konrad Zuse's Plankalkül
1972 • Tarjan's DFS Applications
The central question DFS addresses is this: given a graph — a collection of nodes (also called vertices) connected by edges — how can we systematically visit every reachable node? Unlike its sibling algorithm Breadth-First Search (BFS), DFS commits to one path as far as it can go before backtracking to explore alternatives. This "go deep first" strategy makes DFS ideal for detecting cycles, solving puzzles, and exploring tree structures.
SECTION 2
Core Principles & Definitions
Before diving into code, you need to understand the foundational ideas behind DFS. At its heart, the algorithm relies on a simple rule: always explore the deepest unexplored neighbor first. When you reach a dead end — a node whose neighbors have all been visited — you backtrack to the most recent node that still has unvisited neighbors. This behavior maps perfectly onto a data structure called a stack, which follows the Last-In, First-Out (LIFO) principle.
Graph
Stack (LIFO)
Recursion
Visited Set
Backtracking
KEY TAKEAWAY
SECTION 3
Visual Explanation — How DFS Traverses a Graph
The following diagram shows a simple undirected graph with six nodes labeled A through F. The numbered arrows show the order in which DFS visits each node when starting from node A. Notice how the algorithm goes as deep as possible along one branch before backtracking to explore other branches.
Notice the key pattern: DFS doesn't visit node C (which is directly connected to A) until after it has fully explored the branch starting at B. This is the defining characteristic of depth-first traversal. The algorithm commits fully to one branch before exploring siblings. Compare this to how you might read a book: DFS reads the entire first chapter, including all sub-sections, before moving to chapter two.
SECTION 4
How DFS Works — Recursion vs. Explicit Stack
There are two standard ways to implement DFS in Java. The first uses recursion, where the method calls itself for each unvisited neighbor. The Java runtime maintains an internal call stack that automatically handles the backtracking. The second approach uses an explicit stack — a Stack<Integer> object — where you manually push and pop nodes. Both approaches produce the same traversal, but they have different trade-offs that we'll explore.
The Recursive DFS Algorithm
The recursive version is elegant and compact. Here is the pseudocode that drives every recursive DFS implementation:
RECURSIVE DFS PSEUDOCODE
node = the current vertex being explored. visited = a Set that tracks which nodes have been seen. neighbor = a node directly connected to the current node by an edge.
The Iterative DFS Algorithm (Explicit Stack)
ITERATIVE DFS PSEUDOCODE
stack = an explicit Stack object that replaces the call stack. pop() removes the top element (LIFO order). push() adds a neighbor to the top of the stack.
Time and Space Complexity
TIME COMPLEXITY
V = number of vertices (nodes). E = number of edges. Each vertex is visited once and each edge is examined once, giving linear time.
SPACE COMPLEXITY
In the worst case, all V nodes could end up on the stack (or call stack) at once — for example, in a straight-line graph with no branching.
SECTION 5
Java Implementation — Line by Line
Now let's translate the pseudocode into real Java. We'll represent the graph using an adjacency list — a Map<Integer, List<Integer>> where each key is a node and its value is a list of neighboring nodes. This is the most common graph representation you'll encounter in Java.
StackOverflowError on very deep graphs. The iterative version (right) uses a Stack object on the heap, which can grow larger than the call stack.Let's break down the critical parts. In the recursive version, the line visited.add(node) marks the current node immediately upon entry, preventing the algorithm from revisiting it. The for loop iterates over all neighbors, and the recursive call dfsRecursive(graph, neighbor, visited) dives one level deeper. When that call returns, the loop continues to the next neighbor — this is the backtracking step.
In the iterative version, the while (!stack.isEmpty()) loop replaces the recursion. After popping a node, we check if it's already visited (since the same node could have been pushed multiple times by different neighbors). The getOrDefault method is a safe way to retrieve the neighbor list — it returns an empty list if the node has no outgoing edges, avoiding a NullPointerException.
Java Import Tip
import java.util.Stack; and import java.util.HashSet;. For the graph itself, use import java.util.Map;, import java.util.HashMap;, and import java.util.List;. Modern Java also supports Deque<Integer> with ArrayDeque as a faster alternative to Stack.SECTION 6
Worked Example — Tracing DFS Step by Step
Let's trace the iterative DFS algorithm on a small graph with 5 nodes: 0, 1, 2, 3, and 4. The edges are: 0−1, 0−2, 1−3, 1−4, and 2−4. We start from node 0.
Iterative DFS Trace — Starting from Node 0
- 1
Step 1 — Initialize
Create an empty stack and push the start node 0. Create an empty visited set. Stack:[0]. Visited:{}.Stack = [0], Visited = {} - 2
Step 2 — Pop 0, Mark Visited, Push Neighbors
Pop 0 from the stack. It's not in visited, so mark it visited and print it. Node 0's neighbors are 1 and 2. Push both onto the stack. Stack:[1, 2](2 is on top). Visited:{0}.Output so far: 0 - 3
Step 3 — Pop 2, Mark Visited, Push Neighbors
Pop 2 (top of stack). Not visited, so mark it. Node 2's neighbor is 4. Push 4. Stack:[1, 4]. Visited:{0, 2}.Output so far: 0 2 - 4
Step 4 — Pop 4, Mark Visited, Push Neighbors
Pop 4. Not visited, so mark it. Node 4's neighbors are 1 and 2. Push both, but 2 is already visited — it will be skipped when popped. Stack:[1, 1, 2]. Visited:{0, 2, 4}.Output so far: 0 2 4 - 5
Step 5 — Pop 2 (skip), Pop 1, Pop 1 (skip) — Continue Until Empty
Pop 2 — already visited, skip. Pop 1 — not visited, mark it, print it. Push 1's neighbors 3 and 4. Pop 4 — already visited, skip. Pop 3 — not visited, mark it, print it. Pop 1 — already visited, skip. Stack is now empty and the algorithm terminates.Final output: 0 2 4 1 3
Order May Vary
getOrDefault returns neighbors. Both are valid DFS traversals as long as the depth-first property holds.SECTION 7
DFS: Recursion vs. Stack — Strengths & Limitations
Both implementations have the same time and space complexity, but they behave differently in practice. The table below summarizes the key trade-offs you should consider when choosing between them.
| Criterion | Recursive DFS | Iterative DFS (Explicit Stack) |
|---|---|---|
| Code Length | Short and elegant — about 8−10 lines | Slightly longer — about 12−15 lines |
| Stack Type | Implicit (Java call stack, limited to ~5,000−10,000 frames) | Explicit (heap-allocated, can grow much larger) |
| Risk of Overflow | StackOverflowError on deep graphs | Handles deep graphs safely |
| Ease of Understanding | More intuitive if you're comfortable with recursion | Easier to debug and trace step by step |
| Best For | Trees, small graphs, coding interviews | Large graphs, production code, avoiding recursion limits |
Comparison of Recursive and Iterative DFS Implementations
KEY TAKEAWAY
SECTION 8
Connection to Advanced Topics
DFS is not just a standalone algorithm — it's a building block for many advanced techniques in computer science. Once you're comfortable with basic DFS traversal, you can extend it to solve significantly harder problems. The table below previews some of these connections.
| Basic DFS Concept | Advanced Application |
|---|---|
| Visiting all reachable nodes | Connected components — run DFS from each unvisited node to find isolated clusters |
| Backtracking after dead ends | Topological sort — order tasks by dependencies using post-order DFS |
| Tracking visited nodes | Cycle detection — detect if a graph contains loops using a "currently exploring" state |
| Exploring neighbors recursively | Maze solving and pathfinding — find a path from start to goal in a grid |
| Depth-first traversal order | Tarjan's algorithm — find strongly connected components in directed graphs |
From Basic DFS to Advanced Graph Algorithms
You should also know that DFS has a counterpart called Breadth-First Search (BFS), which uses a queue instead of a stack. BFS explores all neighbors at the current depth before going deeper, making it ideal for finding the shortest path in unweighted graphs. Together, DFS and BFS form the two fundamental graph traversal strategies that you'll use throughout your computer science journey.
SECTION 9
Practice Problems
Question 1: CONCEPTUAL
Show answer
Question 2: BASIC CALCULATION
Show answer
Question 3: INTERMEDIATE
if (!visited.contains(node)) after popping from the stack, instead of before pushing? What would go wrong if we only checked before pushing?Show answer
Question 4: APPLIED
Set<Integer> of all user IDs in the same component as the given user.Show answer
Set<Integer> findFriendGroup(Map<Integer, List<Integer>> graph, int userId) that initializes a HashSet<Integer> visited and calls a recursive helper dfs(graph, userId, visited). The helper marks the node visited, then loops through each neighbor — if the neighbor isn't visited, it recurses. After the initial DFS call returns, the visited set contains every user reachable from userId, which is exactly the friend group. This works because DFS explores all reachable nodes from a starting point, which is the definition of a connected component.Question 5: CRITICAL THINKING
Show answer
SUMMARY
Summary — Depth-First Search in Java
Summary
Depth-First Search (DFS) is a fundamental graph traversal algorithm that explores as far as possible along each branch before backtracking. It relies on a stack (LIFO) data structure — either the implicit call stack through recursion, or an explicit Stack object in an iterative loop. A visited set prevents infinite loops in graphs with cycles.
Both implementations run in O(V + E) time and O(V) space. The recursive version is more concise but risks StackOverflowError on deep graphs. The iterative version handles larger graphs safely. DFS is the foundation for advanced algorithms including topological sort, cycle detection, connected components, and maze solving. In Java, graphs are typically represented as an adjacency list using Map<Integer, List<Integer>>.