DFS Fundamentals and Implementation
How DFS Works
DFS explores as far as possible along each branch before backtracking. It uses a stack (LIFO) or recursion (call stack).
Graph: 0
/ \
1 2
/ \\ \
3 4 5
DFS Order (starting at 0): 0 → 1 → 3 → 4 → 2 → 5
(Goes deep first, then backtracks)
Recursive DFS
import java.util.*;
public class DFS {
// Simple recursive DFS
public void dfsRecursive(Map<Integer, List<Integer>> graph, int node, Set<Integer> visited) {
visited.add(node);
System.out.print(node + " ");
for (int neighbor : graph.get(node)) {
if (!visited.contains(neighbor)) {
dfsRecursive(graph, neighbor, visited);
}
}
}
// DFS that returns a list
public List<Integer> dfs(Map<Integer, List<Integer>> graph, int start) {
List<Integer> result = new ArrayList<>();
Set<Integer> visited = new HashSet<>();
dfsHelper(graph, start, visited, result);
return result;
}
private void dfsHelper(Map<Integer, List<Integer>> graph, int node,
Set<Integer> visited, List<Integer> result) {
visited.add(node);
result.add(node);
for (int neighbor : graph.get(node)) {
if (!visited.contains(neighbor)) {
dfsHelper(graph, neighbor, visited, result);
}
}
}
}
Iterative DFS (Using Stack)
public List<Integer> dfsIterative(Map<Integer, List<Integer>> graph, int start) {
List<Integer> result = new ArrayList<>();
Set<Integer> visited = new HashSet<>();
Stack<Integer> stack = new Stack<>();
stack.push(start);
while (!stack.isEmpty()) {
int node = stack.pop();
if (visited.contains(node)) continue;
visited.add(node);
result.add(node);
// Push neighbors in reverse order for consistent order
List<Integer> neighbors = graph.get(node);
for (int i = neighbors.size() - 1; i >= 0; i--) {
if (!visited.contains(neighbors.get(i))) {
stack.push(neighbors.get(i));
}
}
}
return result;
}
DFS Template for Interview Problems
// Complete DFS template with path tracking
public boolean dfsPath(Map<Integer, List<Integer>> graph, int start, int target) {
Set<Integer> visited = new HashSet<>();
return dfsHelper(graph, start, target, visited);
}
private boolean dfsHelper(Map<Integer, List<Integer>> graph, int current,
int target, Set<Integer> visited) {
if (current == target) return true;
visited.add(current);
for (int neighbor : graph.get(current)) {
if (!visited.contains(neighbor)) {
if (dfsHelper(graph, neighbor, target, visited)) {
return true;
}
}
}
return false;
}
Complexity Analysis
| Operation | Time | Space |
|---|---|---|
| DFS Traversal | O(V + E) | O(V) |
| Path Finding | O(V + E) | O(V) |
| Cycle Detection | O(V + E) | O(V) |
When to Use DFS
- Cycle Detection: Detecting cycles in directed/undirected graphs
- Connected Components: Counting/finding components
- Topological Sort: Ordering with dependencies
- Path Finding: Checking if path exists
- Backtracking: Exploring all possibilities
- Memoization: DFS + memoization (top-down dynamic programming)
Advanced DFS Patterns
Cycle Detection with DFS
Undirected Graph Cycle Detection
public boolean hasCycleUndirected(Map<Integer, List<Integer>> graph, int n) {
boolean[] visited = new boolean[n];
for (int i = 0; i < n; i++) {
if (!visited[i]) {
if (dfsCycleUndirected(graph, i, -1, visited)) {
return true;
}
}
}
return false;
}
private boolean dfsCycleUndirected(Map<Integer, List<Integer>> graph,
int node, int parent, boolean[] visited) {
visited[node] = true;
for (int neighbor : graph.get(node)) {
if (!visited[neighbor]) {
if (dfsCycleUndirected(graph, neighbor, node, visited)) {
return true;
}
} else if (neighbor != parent) {
return true; // Found cycle
}
}
return false;
}
Directed Graph Cycle Detection (3-Color Method)
public boolean hasCycleDirected(Map<Integer, List<Integer>> graph, int n) {
int[] color = new int[n]; // 0: unvisited, 1: visiting, 2: visited
for (int i = 0; i < n; i++) {
if (color[i] == 0) {
if (dfsCycleDirected(graph, i, color)) {
return true;
}
}
}
return false;
}
private boolean dfsCycleDirected(Map<Integer, List<Integer>> graph,
int node, int[] color) {
color[node] = 1; // Mark as visiting
for (int neighbor : graph.get(node)) {
if (color[neighbor] == 1) {
return true; // Back edge found - cycle!
}
if (color[neighbor] == 0) {
if (dfsCycleDirected(graph, neighbor, color)) {
return true;
}
}
}
color[node] = 2; // Mark as visited
return false;
}
Number of Connected Components
public int countComponents(Map<Integer, List<Integer>> graph, int n) {
boolean[] visited = new boolean[n];
int components = 0;
for (int i = 0; i < n; i++) {
if (!visited[i]) {
components++;
dfsMarkComponent(graph, i, visited);
}
}
return components;
}
private void dfsMarkComponent(Map<Integer, List<Integer>> graph,
int node, boolean[] visited) {
visited[node] = true;
for (int neighbor : graph.get(node)) {
if (!visited[neighbor]) {
dfsMarkComponent(graph, neighbor, visited);
}
}
}
DFS on Grid (Flood Fill)
// Flood fill / Island counting
public void dfsGrid(int[][] grid, int r, int c, boolean[][] visited) {
int rows = grid.length, cols = grid[0].length;
if (r < 0 || r >= rows || c < 0 || c >= cols
|| visited[r][c] || grid[r][c] == 0) {
return;
}
visited[r][c] = true;
int[][] dirs = {{-1,0}, {1,0}, {0,-1}, {0,1}};
for (int[] dir : dirs) {
dfsGrid(grid, r + dir[0], c + dir[1], visited);
}
}
// Count islands
public int numIslands(char[][] grid) {
int count = 0;
boolean[][] visited = new boolean[grid.length][grid[0].length];
for (int r = 0; r < grid.length; r++) {
for (int c = 0; c < grid[0].length; c++) {
if (grid[r][c] == '1' && !visited[r][c]) {
count++;
dfsGrid(grid, r, c, visited);
}
}
}
return count;
}
DFS with Backtracking
// All paths from source to target
public List<List<Integer>> allPaths(Map<Integer, List<Integer>> graph,
int start, int target) {
List<List<Integer>> result = new ArrayList<>();
List<Integer> path = new ArrayList<>();
path.add(start);
dfsAllPaths(graph, start, target, path, result);
return result;
}
private void dfsAllPaths(Map<Integer, List<Integer>> graph, int current,
int target, List<Integer> path,
List<List<Integer>> result) {
if (current == target) {
result.add(new ArrayList<>(path));
return;
}
for (int neighbor : graph.get(current)) {
path.add(neighbor);
dfsAllPaths(graph, neighbor, target, path, result);
path.remove(path.size() - 1); // Backtrack
}
}
Common DFS Problems
| Problem | Pattern |
|---|---|
| Number of Islands | DFS on grid |
| Course Schedule | Cycle detection |
| Clone Graph | DFS with visited map |
| Binary Tree Paths | DFS with backtracking |
| Pacific Atlantic Water Flow | DFS from boundaries |
| /accounts-merge | Connected components |
Interactive Visualization
Depth-First Search (DFS) Traversal
Practice Problems
Given a 2D grid of '1's (land) and '0's (water), count the number of islands. An island is formed by connecting adjacent lands horizontally or vertically.
Example:
Input: grid = [["1","1","1","1","0"],["1","1","0","1","0"],["1","1","0","0","0"],["0","0","0","0","0"]]
Output: 1
All '1's are connected, forming one island.
Optimal Solution — O(m×n) time, O(m×n) space
DFS: mark visited cells by sinking island
class Solution {
public int numIslands(char[][] grid) {
int count = 0;
for (int r = 0; r < grid.length; r++) {
for (int c = 0; c < grid[0].length; c++) {
if (grid[r][c] == '1') {
count++;
dfs(grid, r, c);
}
}
}
return count;
}
private void dfs(char[][] grid, int r, int c) {
if (r < 0 || r >= grid.length || c < 0 || c >= grid[0].length || grid[r][c] == '0') return;
grid[r][c] = '0';
dfs(grid, r+1, c); dfs(grid, r-1, c);
dfs(grid, r, c+1); dfs(grid, r, c-1);
}
}Edge Cases:
- Empty grid
- No land
- All land
Given a reference of a node in a connected undirected graph, return a deep copy of the graph.
Example:
Input: adjList = [[2,4],[1,3],[2,4],[1,3]]
Output: Same structure cloned
Clone each node and its neighbors.
Optimal Solution — O(V+E) time, O(V) space
DFS with HashMap to map original to cloned nodes
class Solution {
Map<Node, Node> map = new HashMap<>();
public Node cloneGraph(Node node) {
if (node == null) return null;
if (map.containsKey(node)) return map.get(node);
Node clone = new Node(node.val);
map.put(node, clone);
for (Node neighbor : node.neighbors) {
clone.neighbors.add(cloneGraph(neighbor));
}
return clone;
}
}Edge Cases:
- Empty graph
- Single node
- Disconnected components
Given an m x n matrix of heights, return a list of coordinates where water can flow to both Pacific and Atlantic oceans.
Example:
Input: heights = [[1,2,2,3,5],[3,2,3,4,4],[2,4,5,3,1],[6,7,1,4,5]]
Output: [[0,4],[1,3],[1,4],[2,2],[3,0],[3,1],[4,0]]
Water from these cells can reach both oceans.
Optimal Solution — O(m×n) time, O(m×n) space
Reverse DFS from ocean borders, find intersection
class Solution {
public List<List<Integer>> pacificAtlantic(int[][] heights) {
int m = heights.length, n = heights[0].length;
boolean[][] pacific = new boolean[m][n];
boolean[][] atlantic = new boolean[m][n];
for (int i = 0; i < m; i++) {
dfs(heights, pacific, i, 0, Integer.MIN_VALUE);
dfs(heights, atlantic, i, n-1, Integer.MIN_VALUE);
}
for (int j = 0; j < n; j++) {
dfs(heights, pacific, 0, j, Integer.MIN_VALUE);
dfs(heights, atlantic, m-1, j, Integer.MIN_VALUE);
}
List<List<Integer>> result = new ArrayList<>();
for (int i = 0; i < m; i++)
for (int j = 0; j < n; j++)
if (pacific[i][j] && atlantic[i][j])
result.add(Arrays.asList(i, j));
return result;
}
private void dfs(int[][] h, boolean[][] visited, int r, int c, int prev) {
if (r < 0 || r >= h.length || c < 0 || c >= h[0].length) return;
if (visited[r][c] || h[r][c] < prev) return;
visited[r][c] = true;
int[][] dirs = {{-1,0},{1,0},{0,-1},{0,1}};
for (int[] d : dirs) dfs(h, visited, r+d[0], c+d[1], h[r][c]);
}
}Edge Cases:
- Single cell
- All same height
- 1x1 matrix
Quiz
1. What is the key difference between DFS cycle detection in directed vs undirected graphs?
2. When is DFS preferred over BFS?
3. What is the primary purpose of Depth-First Search (DFS)?
4. What is a common mistake when implementing Depth-First Search (DFS)?
Flashcards
Question
What is the 3-color method in directed graph cycle detection?
Click to reveal answer
Answer
Color 0: Unvisited. Color 1: Currently in recursion stack (visiting). Color 2: Fully processed (visited). If we reach a Color 1 node, we found a cycle (back edge).
Question
How do you modify DFS to find all paths between two nodes?
Click to reveal answer
Answer
Use backtracking: add current node to path, recurse on neighbors, then remove current node (backtrack) before exploring next neighbor. Save path when target is reached.
Question
What is Depth-First Search (DFS)?
Click to reveal answer
Answer
Depth-First Search (DFS) is a key concept in software engineering.
Question
When to use Depth-First Search (DFS)?
Click to reveal answer
Answer
Use Depth-First Search (DFS) when building production systems that require reliability, scalability, and maintainability.
Question
Depth-First Search (DFS) best practices
Click to reveal answer
Answer
Follow SOLID principles, write clean code, test thoroughly, document decisions, and monitor in production.
Revision Notes
Key Takeaways
- 1.DFS uses recursion or stack, explores as deep as possible first
- 2.3-color method for directed graph cycle detection
- 3.Track parent in undirected graph to avoid false cycle detection
- 4.DFS + backtracking finds all paths between nodes
- 5.For grids, use direction arrays and boundary checks
Interview Tips
- •DFS is natural for recursion problems and backtracking
- •Use 3-color method for directed graph cycles
- •For island problems, modify grid in-place to avoid extra visited array
- •DFS + memoization = top-down dynamic programming
- •Be careful of stack overflow for very deep graphs - use iterative DFS
Cheat Sheet
DFS Cheat Sheet
Recursive DFS Template
void dfs(int node, boolean[] visited) {
visited[node] = true;
for (int neighbor : graph.get(node)) {
if (!visited[neighbor]) {
dfs(neighbor, visited);
}
}
}
Iterative DFS Template
Stack<Integer> stack = new Stack<>();
stack.push(start);
while (!stack.isEmpty()) {
int node = stack.pop();
if (visited[node]) continue;
visited[node] = true;
for (int neighbor : graph.get(node)) {
if (!visited[neighbor]) stack.push(neighbor);
}
}
Cycle Detection
Undirected (with parent tracking):
boolean dfs(int node, int parent) {
visited[node] = true;
for (int neighbor : graph.get(node)) {
if (!visited[neighbor]) {
if (dfs(neighbor, node)) return true;
} else if (neighbor != parent) {
return true; // Cycle!
}
}
return false;
}
Directed (3-color method):
// 0=unvisited, 1=visiting, 2=visited
boolean dfs(int node) {
color[node] = 1; // Mark visiting
for (int neighbor : graph.get(node)) {
if (color[neighbor] == 1) return true; // Back edge!
if (color[neighbor] == 0 && dfs(neighbor)) return true;
}
color[node] = 2; // Mark visited
return false;
}
Grid DFS Directions
int[][] dirs = {{-1,0}, {1,0}, {0,-1}, {0,1}};
void dfs(char[][] grid, int r, int c) {
if (r < 0 || r >= rows || c < 0 || c >= cols || grid[r][c] == '0') return;
grid[r][c] = '0'; // Mark visited
for (int[] d : dirs) dfs(grid, r+d[0], c+d[1]);
}
Complexity
- Time: O(V + E)
- Space: O(V) for visited, O(H) for recursion stack (H = height)