Depth-First Search (DFS) - Complete Guide
Depth-First Search (DFS) - Complete Guide
Introduction
Depth-First Search (DFS) is a fundamental graph traversal algorithm that explores as far as possible along each branch before backtracking. It is a "blind" search, meaning it explores the graph without any specific direction or heuristic, prioritizing depth over breadth.
The Core Concept
Imagine you are exploring a maze. If you choose to go down the first corridor you see and keep walking until you hit a dead end, then backtrack to the last junction and try the next corridor, you are performing a DFS. This "go deep, then backtrack" strategy is the essence of DFS.
Graph Representation
Before implementing DFS, we need a way to represent the graph. The most common representation is the Adjacency List.
interface Graph {
[key: string]: string[];
}
const graph: Graph = {
A: ['B', 'C'],
B: ['A', 'D', 'E'],
C: ['A', 'F'],
D: ['B'],
E: ['B', 'F'],
F: ['C', 'E']
};How DFS Works
DFS uses a Stack data structure (Last-In, First-Out) to keep track of the nodes to visit next. Alternatively, it can be implemented using Recursion.
1. Iterative Approach (Using Stack)
- Push the starting node onto the stack.
- While the stack is not empty:
a. Pop a node from the stack.
b. If the node has not been visited:
i. Mark it as visited.
ii. Print/process the node.
iii. Push all its unvisited neighbors onto the stack.
2. Recursive Approach
- Mark the current node as visited.
- Recursively call DFS for all adjacent nodes that haven't been visited.
Implementation
Iterative Implementation in TypeScript
function dfsIterative(graph: Graph, startNode: string): void {
const visited = new Set<string>();
const stack: string[] = [startNode];
while (stack.length > 0) {
const currentNode = stack.pop();
if (currentNode && !visited.has(currentNode)) {
visited.add(currentNode);
console.log(`Visited: ${currentNode}`);
// Push neighbors in reverse order to maintain left-to-right traversal
for (const neighbor of graph[currentNode]) {
if (!visited.has(neighbor)) {
stack.push(neighbor);
}
}
}
}
}Recursive Implementation in TypeScript
function dfsRecursive(graph: Graph, node: string, visited: Set<string> = new Set()): void {
if (visited.has(node)) {
return;
}
visited.add(node);
console.log(`Visited: ${node}`);
for (const neighbor of graph[node]) {
dfsRecursive(graph, neighbor, visited);
}
}Complexity Analysis
Time Complexity: O(V + E)
- V is the number of vertices (nodes).
- E is the number of edges.
- In the worst case, we visit every node and every edge exactly once.
Space Complexity: O(V)
- Stack Space: In the worst case (a linear chain graph), the stack or recursion depth will hold all V nodes.
- Visited Set: We store V nodes in the visited set.
Applications of DFS
- Pathfinding: Finding a path between two nodes in a graph.
- Topological Sorting: Ordering vertices in a directed acyclic graph (DAG) such that for every directed edge (u, v), vertex u comes before v in the ordering.
- Cycle Detection: Detecting cycles in a graph (both directed and undirected).
- Connected Components: Finding all connected components in an undirected graph.
- Maze Generation: Generating random mazes.
DFS vs. BFS
| Feature | Depth-First Search (DFS) | Breadth-First Search (BFS) |
| :--- | :--- | :--- |
| Data Structure | Stack (Iterative) or Recursion | Queue |
| Order of Traversal | Depth-first (go deep first) | Breadth-first (level by level) |
| Memory Usage | Lower (only stores path to root) | Higher (stores all nodes at current level) |
| Best For | Finding paths in deep graphs, Topological Sort | Shortest path in unweighted graphs, Finding nodes at a specific level |
Common Use Cases in Interviews
- Number of Islands: Given a 2D grid of '1's (land) and '0's (water), count the number of islands. An island is surrounded by water and is formed by connecting adjacent lands horizontally or vertically.
- Clone Graph: Clone an undirected graph.
- Course Schedule: Determine if you can finish all courses given prerequisites.
- Surrounded Regions: Replace all 'O's surrounded by 'X's with 'X's.
Common Pitfalls
- Infinite Loops: Forgetting to mark a node as visited before pushing its neighbors to the stack/queue can lead to infinite loops, especially in graphs with cycles.
- Stack Overflow: In languages with limited stack size (like Java or Python), deep recursion can cause a stack overflow error for very large graphs. Prefer the iterative approach for massive graphs.
- Graph Representation: Using an Adjacency Matrix instead of an Adjacency List can lead to O(V^2) time complexity if not optimized, whereas DFS is naturally O(V + E).
Summary
DFS is a versatile algorithm that prioritizes depth. It is memory-efficient for deep graphs and is the go-to choice for problems involving topological sorting, cycle detection, and pathfinding where the shortest path is not the primary concern.