Graph Traversals — The Complete Guide
Master BFS and DFS from first principles. Learn to model problems as graphs, choose the right traversal, handle cycles and components, and confidently solve interview questions involving connectivity, shortest paths, and grid exploration.
Table of Contents
Intuition from Scratch
Why does this pattern exist?
Arrays are linear — data sits in a row. Trees are hierarchical — data branches downward. But many real-world relationships are neither linear nor hierarchical. Cities connected by roads, people connected by friendships, web pages connected by links — these are graphs. A graph is just a set of nodes (vertices) connected by edges, with no restriction on shape. Any node can connect to any other node, and there can be cycles.
Graph traversal answers the most fundamental question: starting from a node, how do I systematically visit every reachable node exactly once? There are two strategies. BFS (Breadth-First Search) explores level by level — visit all neighbors first, then neighbors of neighbors. DFS (Depth-First Search) explores as deep as possible along one path before backtracking. Both visit every reachable node, but they do it in different orders, which makes each one better suited for different problems.
Graph traversals are the foundation for almost every graph algorithm: shortest paths, cycle detection, connected components, topological sort, bipartite checking, and more. If you can model a problem as a graph and traverse it correctly, you can solve a huge category of interview questions.
Analogies to Build Intuition
BFS — Ripples in a Pond
Drop a stone in a pond. Ripples spread outward in concentric circles — first the closest water, then the next ring, then the next. BFS works the same way: it visits all nodes at distance 1, then distance 2, then distance 3. This is why BFS finds the shortest path in unweighted graphs — it reaches each node at the earliest possible 'ripple.'
DFS — Exploring a Maze with String
You enter a maze holding a ball of string. At each fork, you pick one path and keep going deeper. When you hit a dead end, you follow the string back to the last fork and try the next path. You explore one complete branch before trying another. That's DFS — go deep first, backtrack when stuck.
Graph as a Map
Think of a graph as a map of cities (nodes) connected by roads (edges). BFS is like sending scouts in all directions simultaneously — they spread outward evenly. DFS is like sending one scout who follows a single road as far as possible before turning back. Both eventually map the entire reachable territory.
What kind of problems does it solve?
Graph traversals are your go-to when:
- You need to find the shortest path in an unweighted graph (BFS)
- You need to count connected components or islands
- You need to detect cycles in a directed or undirected graph
- You need to check if a graph is bipartite (2-colorable)
- You need to flood-fill a grid or explore a matrix
- You need topological ordering of tasks with dependencies
- The problem involves reachability — can you get from A to B?
The core insight
BFS and DFS are two ways to systematically visit every reachable node. BFS explores by distance (level by level) — use it when you need shortest paths or level-order information. DFS explores by depth (one branch at a time) — use it when you need to explore all paths, detect cycles, or work with recursion. Both use a "visited" set to avoid revisiting nodes in graphs with cycles.
Pattern Recognition
Recognizing when a problem is a graph problem — and choosing BFS vs DFS — is the most important skill. Many problems don't mention "graph" at all but are graph problems in disguise.
Keywords & Signals in the Problem Statement
Use Graph Traversal when you see...
- ✅"Number of islands" — grid = implicit graph
- ✅"Shortest path" in an unweighted graph → BFS
- ✅"Connected components" or "number of provinces"
- ✅"Can you reach from A to B?" — reachability
- ✅"Clone a graph" — traverse and copy
- ✅"Course schedule" — dependency graph → topological sort
- ✅"Bipartite" or "2-colorable" graph
- ✅"Flood fill" or "surrounded regions" on a grid
- ✅"Word ladder" — transform one word to another → BFS
- ✅A 2D grid where you move up/down/left/right
Don't use Graph Traversal when...
- ❌The problem is about a sorted array — use Binary Search
- ❌You need shortest path in a WEIGHTED graph — use Dijkstra or Bellman-Ford
- ❌The problem is about contiguous subarrays — use Sliding Window
- ❌The structure is a tree (no cycles) — simpler tree DFS/BFS suffices
- ❌You need to optimize over subsets — use DP or Backtracking
- ❌The problem is purely mathematical — no graph structure
BFS vs. DFS — When to Use Which
| Criterion | BFS | DFS |
|---|---|---|
| Shortest path (unweighted) | ✅ Guaranteed — explores by distance | ❌ Not guaranteed — may find a longer path first |
| Level-order / distance info | ✅ Natural — processes level by level | ❌ Doesn't track levels naturally |
| Cycle detection | ✅ Works (check if neighbor is already visited) | ✅ Works (back edge = cycle in DFS tree) |
| Topological sort | ✅ Kahn's algorithm (BFS with in-degree) | ✅ Post-order DFS (reverse finish order) |
| Connected components | ✅ Works | ✅ Works (often simpler to code) |
| Path finding / backtracking | ❌ Hard to reconstruct paths | ✅ Natural — recursion tracks the path |
| Space complexity | O(width) — can be large for wide graphs | O(depth) — can be large for deep graphs |
| Implementation | Queue (iterative) | Stack or recursion |
The BFS vs DFS decision
Default rule: use BFS when you need shortest path or level-order info. Use DFS for everything else (connected components, cycle detection, path exploration). In grid problems, both work for counting islands — DFS is usually simpler to code. For "minimum steps to reach X," always use BFS.
Brute Force → Optimized Thinking
Let's walk through the thinking evolution using a classic problem: 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.
Brute Force — Check Every Cell Independently
For each land cell, do a full scan to determine which island it belongs to. Compare every pair of land cells to see if they're connected. This is essentially computing connected components by brute-force pair comparison — extremely slow.
For each cell (i, j) that is '1':
For each other cell (x, y) that is '1':
Check if there's a path from (i,j) to (x,y)
If yes, they're in the same island
Problem: Checking paths between all pairs is O((m×n)²) at best.
We're doing way too much redundant work.
Optimal — DFS/BFS Flood Fill
The key insight: when you find a land cell, explore the entire island from that cell using DFS or BFS (flood fill). Mark every cell in the island as visited. Then move on to find the next unvisited land cell — that's a new island. Each cell is visited exactly once.
function numIslands(grid: string[][]): number {
const rows = grid.length;
const cols = grid[0].length;
let islands = 0;
function dfs(r: number, c: number) {
// Out of bounds or water or already visited
if (r < 0 || r >= rows || c < 0 || c >= cols) return;
if (grid[r][c] !== '1') return;
grid[r][c] = '0'; // mark visited (sink the land)
// Explore all 4 directions
dfs(r + 1, c);
dfs(r - 1, c);
dfs(r, c + 1);
dfs(r, c - 1);
}
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
if (grid[r][c] === '1') {
islands++; // found a new island
dfs(r, c); // sink the entire island
}
}
}
return islands;
}
// Each cell is visited once by the outer loop and at most once by DFS.
// Total: O(m × n) time, O(m × n) stack space in worst case.
Why Does the Optimization Work?
Flood fill explores the entire connected component
Starting from any land cell, DFS/BFS visits every cell reachable through adjacent land. This is exactly one island. By marking cells as visited (sinking them to '0'), we ensure they're never counted again.
Each cell is processed exactly once
The outer loop touches each cell once. DFS/BFS also touches each cell at most once (because visited cells are skipped). Total work: O(m × n). No redundant pair comparisons.
The general principle
Counting connected components in any graph follows the same pattern: iterate over all nodes, and for each unvisited node, run a traversal (BFS or DFS) to mark the entire component. Increment the count for each new traversal. This works for grids, adjacency lists, and any graph representation.
The thinking process matters more than the code
In interviews, walk through this: "Each island is a connected component. I'll iterate over the grid. When I find an unvisited '1', I increment my island count and flood-fill the entire island using DFS, marking cells as visited. Each cell is processed once, so it's O(m × n)."
Core Templates
Graph traversal problems follow a few recurring templates. Memorize these — most problems are variations of one of them.
Template 1: BFS on a Graph (Adjacency List)
The standard BFS template. Uses a queue to process nodes level by level. Guarantees shortest path in unweighted graphs. The visited set prevents revisiting nodes in cyclic graphs.
function bfs(graph: Map<number, number[]>, start: number): void {
const visited = new Set<number>();
const queue: number[] = [start];
visited.add(start);
while (queue.length > 0) {
const node = queue.shift()!; // dequeue front
// Process node here
for (const neighbor of graph.get(node) ?? []) {
if (!visited.has(neighbor)) {
visited.add(neighbor); // mark BEFORE enqueuing
queue.push(neighbor);
}
}
}
}
// Time: O(V + E) — visit every vertex and edge once
// Space: O(V) — visited set + queue
// Key: mark visited WHEN ENQUEUING, not when dequeuing.
// This prevents the same node from being added to the queue multiple times.
Template 2: DFS on a Graph (Adjacency List)
The standard DFS template. Can be recursive or iterative (with a stack). Explores one branch fully before backtracking. Used for connected components, cycle detection, and path exploration.
function dfs(
graph: Map<number, number[]>,
node: number,
visited: Set<number>
): void {
visited.add(node);
// Process node here
for (const neighbor of graph.get(node) ?? []) {
if (!visited.has(neighbor)) {
dfs(graph, neighbor, visited);
}
}
}
// Iterative version (explicit stack):
function dfsIterative(graph: Map<number, number[]>, start: number): void {
const visited = new Set<number>();
const stack: number[] = [start];
while (stack.length > 0) {
const node = stack.pop()!;
if (visited.has(node)) continue;
visited.add(node);
// Process node here
for (const neighbor of graph.get(node) ?? []) {
if (!visited.has(neighbor)) {
stack.push(neighbor);
}
}
}
}
// Time: O(V + E). Space: O(V) for visited + O(V) stack depth.
Template 3: BFS/DFS on a Grid
Grids are implicit graphs — each cell is a node, and edges connect adjacent cells (up, down, left, right). The directions array and bounds checking replace the adjacency list.
function bfsGrid(grid: string[][], startR: number, startC: number): void {
const rows = grid.length;
const cols = grid[0].length;
const dirs = [[1, 0], [-1, 0], [0, 1], [0, -1]]; // 4 directions
const queue: [number, number][] = [[startR, startC]];
grid[startR][startC] = '0'; // mark visited (or use a separate set)
while (queue.length > 0) {
const [r, c] = queue.shift()!;
for (const [dr, dc] of dirs) {
const nr = r + dr;
const nc = c + dc;
if (nr >= 0 && nr < rows && nc >= 0 && nc < cols && grid[nr][nc] === '1') {
grid[nr][nc] = '0'; // mark visited BEFORE enqueuing
queue.push([nr, nc]);
}
}
}
}
// Grid DFS is the same but uses recursion or a stack instead of a queue.
// The directions array [1,0], [-1,0], [0,1], [0,-1] handles 4-directional movement.
// For 8-directional, add the 4 diagonals.
// When to use: Number of Islands, Flood Fill, Shortest Path in Binary Matrix,
// Rotting Oranges, Surrounded Regions
Template 4: BFS with Level Tracking (Shortest Path)
When you need the shortest distance, track levels explicitly. Process all nodes at the current distance before moving to the next distance. This is the standard pattern for "minimum steps" problems.
function bfsShortestPath(graph: Map<number, number[]>, start: number, target: number): number {
const visited = new Set<number>();
const queue: number[] = [start];
visited.add(start);
let distance = 0;
while (queue.length > 0) {
const levelSize = queue.length; // nodes at current distance
for (let i = 0; i < levelSize; i++) {
const node = queue.shift()!;
if (node === target) return distance; // found!
for (const neighbor of graph.get(node) ?? []) {
if (!visited.has(neighbor)) {
visited.add(neighbor);
queue.push(neighbor);
}
}
}
distance++; // all nodes at current distance processed
}
return -1; // target not reachable
}
// Key: the inner loop processes exactly one "level" (all nodes at the same distance).
// After the inner loop, increment distance.
// This guarantees the first time we reach the target, it's at the shortest distance.
// When to use: Word Ladder, Shortest Path in Binary Matrix, Rotting Oranges,
// minimum steps to reach a state
Which template to pick?
Ask yourself: (1) Do I need shortest path / minimum steps? → Template 4 (BFS with levels). (2) Is the input a grid? → Template 3. (3) Do I need to explore all paths or detect cycles? → Template 2 (DFS). (4) General graph traversal? → Template 1 (BFS) or Template 2 (DFS) — either works.
Variations & Sub-Patterns
Graph traversals aren't a single trick — they're the foundation for a whole family of algorithms. Here are the most common variations.
| Variation | Traversal Strategy | Classic Example |
|---|---|---|
| Connected Components | DFS/BFS from each unvisited node; count traversals | Number of Islands, Number of Provinces |
| Shortest Path (unweighted) | BFS with level tracking; first visit = shortest | Word Ladder, Shortest Path in Binary Matrix |
| Multi-source BFS | Start BFS from ALL sources simultaneously | Rotting Oranges, 01 Matrix, Walls and Gates |
| Cycle Detection (undirected) | DFS: if neighbor is visited and not parent → cycle | Graph Valid Tree, Redundant Connection |
| Cycle Detection (directed) | DFS with 3 colors: white/gray/black; gray→gray = cycle | Course Schedule, Detect Cycle in Directed Graph |
| Topological Sort | BFS (Kahn's) or DFS post-order reverse | Course Schedule II, Alien Dictionary |
| Bipartite Check | BFS/DFS with 2-coloring; conflict = not bipartite | Is Graph Bipartite?, Possible Bipartition |
| Graph Cloning | DFS/BFS + hash map (old node → new node) | Clone Graph |
Problems mentioned above
Deep Dive: Multi-Source BFS — Rotting Oranges
In a grid, every rotten orange infects adjacent fresh oranges each minute. Find the minimum time until all oranges are rotten (or -1 if impossible). The trick: start BFS from ALL rotten oranges simultaneously. Each BFS level = one minute.
function orangesRotting(grid: number[][]): number {
const rows = grid.length, cols = grid[0].length;
const queue: [number, number][] = [];
let fresh = 0;
// Enqueue ALL rotten oranges as starting points
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
if (grid[r][c] === 2) queue.push([r, c]);
if (grid[r][c] === 1) fresh++;
}
}
if (fresh === 0) return 0; // no fresh oranges
const dirs = [[1,0],[-1,0],[0,1],[0,-1]];
let minutes = 0;
while (queue.length > 0 && fresh > 0) {
const levelSize = queue.length;
for (let i = 0; i < levelSize; i++) {
const [r, c] = queue.shift()!;
for (const [dr, dc] of dirs) {
const nr = r + dr, nc = c + dc;
if (nr >= 0 && nr < rows && nc >= 0 && nc < cols && grid[nr][nc] === 1) {
grid[nr][nc] = 2; // rot it
fresh--;
queue.push([nr, nc]);
}
}
}
minutes++;
}
return fresh === 0 ? minutes : -1;
}
// Multi-source BFS: all rotten oranges spread simultaneously.
// Each BFS level = 1 minute. When fresh === 0, we're done.
// Time: O(m × n). Space: O(m × n) for the queue.
Deep Dive: Topological Sort — Course Schedule II
Given n courses and prerequisites, return a valid order to take all courses (or empty if impossible). This is topological sort on a directed graph. Kahn's algorithm (BFS) processes nodes with in-degree 0 first.
function findOrder(numCourses: number, prerequisites: number[][]): number[] {
// Build adjacency list and in-degree array
const graph = new Map<number, number[]>();
const inDegree = new Array(numCourses).fill(0);
for (const [course, prereq] of prerequisites) {
if (!graph.has(prereq)) graph.set(prereq, []);
graph.get(prereq)!.push(course);
inDegree[course]++;
}
// Start with all courses that have no prerequisites
const queue: number[] = [];
for (let i = 0; i < numCourses; i++) {
if (inDegree[i] === 0) queue.push(i);
}
const order: number[] = [];
while (queue.length > 0) {
const course = queue.shift()!;
order.push(course);
for (const next of graph.get(course) ?? []) {
inDegree[next]--;
if (inDegree[next] === 0) {
queue.push(next); // all prerequisites satisfied
}
}
}
// If we processed all courses, the order is valid
return order.length === numCourses ? order : [];
}
// Time: O(V + E). Space: O(V + E).
// If order.length < numCourses, there's a cycle → impossible.
// Kahn's naturally detects cycles: nodes in a cycle never reach in-degree 0.
Deep Dive: Cycle Detection in Directed Graphs — 3-Color DFS
To detect cycles in a directed graph, use DFS with three states: white (unvisited), gray (in current DFS path), black (fully processed). If you encounter a gray node, you've found a back edge — a cycle.
function hasCycle(numNodes: number, edges: number[][]): boolean {
const graph = new Map<number, number[]>();
for (const [u, v] of edges) {
if (!graph.has(u)) graph.set(u, []);
graph.get(u)!.push(v);
}
// 0 = white (unvisited), 1 = gray (in path), 2 = black (done)
const color = new Array(numNodes).fill(0);
function dfs(node: number): boolean {
color[node] = 1; // gray — currently exploring
for (const neighbor of graph.get(node) ?? []) {
if (color[neighbor] === 1) return true; // back edge → cycle!
if (color[neighbor] === 0 && dfs(neighbor)) return true;
}
color[node] = 2; // black — fully processed
return false;
}
for (let i = 0; i < numNodes; i++) {
if (color[i] === 0 && dfs(i)) return true;
}
return false;
}
// Gray → gray = back edge = cycle in directed graph.
// Black nodes are safe — they've been fully explored with no cycle.
// This is the standard technique for Course Schedule (can you finish all courses?).
Visual Walkthrough
Let's trace through BFS and DFS on the same graph to see how they differ, then walk through a grid problem.
BFS vs DFS on the Same Graph
Graph (undirected):
0 — 1 — 3
| |
2 4
Adjacency list:
0: [1, 2]
1: [0, 3, 4]
2: [0]
3: [1]
4: [1]
BFS from node 0 (queue):
Queue: [0] Visit: 0 → Process 0, enqueue neighbors 1, 2
Queue: [1, 2] Visit: 1 → Process 1, enqueue neighbors 3, 4
Queue: [2, 3, 4] Visit: 2 → Process 2, no new neighbors
Queue: [3, 4] Visit: 3 → Process 3, no new neighbors
Queue: [4] Visit: 4 → Process 4, no new neighbors
BFS order: 0, 1, 2, 3, 4 (level by level)
Level 0: {0}
Level 1: {1, 2}
Level 2: {3, 4}
DFS from node 0 (recursive):
Visit 0 → recurse to neighbor 1
Visit 1 → recurse to neighbor 3
Visit 3 → no unvisited neighbors, backtrack
Back to 1 → recurse to neighbor 4
Visit 4 → no unvisited neighbors, backtrack
Back to 1 → backtrack
Back to 0 → recurse to neighbor 2
Visit 2 → no unvisited neighbors, backtrack
DFS order: 0, 1, 3, 4, 2 (depth first)
Key difference:
BFS: 0 → {1,2} → {3,4} (explores by distance)
DFS: 0 → 1 → 3 → 4 → 2 (explores one branch fully before the next)
Number of Islands — Grid DFS Trace
Grid:
1 1 0 0 0
1 1 0 0 0
0 0 1 0 0
0 0 0 1 1
Scan left-to-right, top-to-bottom:
(0,0) = '1' → NEW ISLAND #1
DFS flood fill: (0,0)→(1,0)→(1,1)→(0,1) all become '0'
Grid after:
0 0 0 0 0
0 0 0 0 0
0 0 1 0 0
0 0 0 1 1
(0,1) to (1,1) = '0' → skip
(2,2) = '1' → NEW ISLAND #2
DFS flood fill: (2,2) becomes '0'
Grid after:
0 0 0 0 0
0 0 0 0 0
0 0 0 0 0
0 0 0 1 1
(3,3) = '1' → NEW ISLAND #3
DFS flood fill: (3,3)→(3,4) both become '0'
Grid after:
0 0 0 0 0
0 0 0 0 0
0 0 0 0 0
0 0 0 0 0
Answer: 3 islands
Key: Each DFS call from the outer loop discovers one complete island.
Sinking land to '0' prevents double-counting.
Topological Sort — Kahn's Algorithm Trace
Courses: 0, 1, 2, 3
Prerequisites: [[1,0], [2,0], [3,1], [3,2]]
Meaning: 0→1, 0→2, 1→3, 2→3
In-degrees: [0, 1, 1, 2]
Course 0: 0 prerequisites
Course 1: 1 prerequisite (course 0)
Course 2: 1 prerequisite (course 0)
Course 3: 2 prerequisites (courses 1 and 2)
Step 1: Queue = [0] (in-degree 0)
Process 0 → order = [0]
Decrement neighbors: inDegree[1]=0, inDegree[2]=0
Queue = [1, 2]
Step 2: Process 1 → order = [0, 1]
Decrement neighbors: inDegree[3]=1
Queue = [2]
Step 3: Process 2 → order = [0, 1, 2]
Decrement neighbors: inDegree[3]=0
Queue = [3]
Step 4: Process 3 → order = [0, 1, 2, 3]
Queue = []
Answer: [0, 1, 2, 3] ✓ (valid topological order)
All 4 courses processed → no cycle.
If a cycle existed, some nodes would never reach in-degree 0,
and order.length < numCourses → return [].
Practice Problems
These 7 problems are carefully ordered from easy to hard. Each one teaches a different graph traversal technique. Solve them in order.
LC 733. Flood Fill
Why this pattern:
Grid DFS/BFS (Template 3). Starting from a pixel, change all connected pixels of the same color to a new color. This is the simplest graph traversal on a grid — pure flood fill with no extra logic.
Key idea: DFS from the starting pixel. For each cell, if it matches the original color, change it to the new color and recurse on all 4 neighbors. The color change acts as the 'visited' marker. Handle the edge case where newColor === originalColor (no-op).
LC 200. Number of Islands
Why this pattern:
Connected components on a grid (Template 3). Each island is a connected component of '1's. Iterate over the grid; for each unvisited '1', increment the count and flood-fill the entire island. The foundational grid graph problem.
Key idea: Outer loop scans every cell. When a '1' is found, increment islands and run DFS/BFS to sink the entire island (mark all connected '1's as '0'). Each cell is visited at most once → O(m × n).
LC 994. Rotting Oranges
Why this pattern:
Multi-source BFS with level tracking (Template 4 variant). All rotten oranges spread simultaneously — this is BFS from multiple sources. Each BFS level = 1 minute. This teaches the critical multi-source BFS pattern.
Key idea: Enqueue ALL rotten oranges at the start. BFS level by level: each level rots adjacent fresh oranges. Track fresh count; when it hits 0, return the number of levels. If fresh > 0 after BFS, return -1.
LC 133. Clone Graph
Why this pattern:
Graph traversal + hash map (DFS/BFS + cloning). Traverse the graph and create a copy of each node. Use a hash map (old node → new node) to avoid duplicating nodes and to handle cycles. This teaches graph cloning with cycle handling.
Key idea: DFS: for each node, if it's already in the map, return the clone. Otherwise, create a new node, add it to the map, then recursively clone all neighbors. The map serves as both the visited set and the old→new mapping.
LC 210. Course Schedule II
Why this pattern:
Topological sort (Kahn's BFS — Template 4 variant). Build a directed graph from prerequisites. Use BFS with in-degree tracking to find a valid course order. If not all courses are processed, there's a cycle.
Key idea: Build adjacency list and in-degree array. Enqueue all nodes with in-degree 0. Process each node: add to order, decrement neighbors' in-degrees. When a neighbor reaches in-degree 0, enqueue it. If order.length < numCourses, cycle exists.
LC 127. Word Ladder
Why this pattern:
BFS shortest path on an implicit graph (Template 4). Each word is a node; edges connect words that differ by one letter. BFS finds the shortest transformation sequence. This teaches modeling non-obvious problems as graphs.
Key idea: BFS from beginWord. For each word, try changing each character to a-z. If the new word is in the word list, it's a neighbor. Use a set for O(1) lookup. BFS with level tracking gives the shortest path. Remove visited words from the set to avoid revisiting.
LC 417. Pacific Atlantic Water Flow
Why this pattern:
Reverse BFS/DFS from borders (multi-source). Instead of checking if each cell can reach both oceans (expensive), start from the ocean borders and flow inward. Cells reachable from both oceans are the answer. This teaches reverse-thinking in graph problems.
Key idea: Run DFS/BFS from all Pacific border cells (top row + left column). Run another from all Atlantic border cells (bottom row + right column). The intersection of both reachable sets is the answer. Reverse flow: move to neighbors with height ≥ current.
Practice strategy
For each problem: (1) Identify the graph — what are nodes and edges? (2) Choose BFS or DFS and justify why. (3) Decide on the visited strategy (modify grid, separate set, or coloring). (4) After solving, write: "This is [BFS/DFS] because [reason]. Nodes are [X], edges are [Y]."
Common Mistakes
These are the traps that catch people on graph traversal problems. Learn them here so you don't learn them in an interview.
Marking visited when DEQUEUING instead of ENQUEUING
In BFS, you mark a node as visited when you pop it from the queue. But by then, the same node may have been enqueued multiple times by different neighbors. This causes duplicate processing and can turn O(V+E) into O(V²).
✅Always mark visited WHEN ENQUEUING (before pushing to the queue), not when dequeuing. This ensures each node enters the queue exactly once. For DFS with an explicit stack, check visited when popping (since the stack may have duplicates).
Infinite loop from missing visited check
In a graph with cycles, you visit node A, which leads to B, which leads back to A. Without a visited set, you loop forever. This is the #1 bug in graph problems.
✅ALWAYS maintain a visited set (or modify the grid in-place). Check it before processing any neighbor. For grids, marking cells as '0' or '#' is the simplest approach. For adjacency lists, use a Set<number>.
Wrong bounds checking in grid problems
You access grid[r-1][c] without checking if r-1 >= 0, causing an index-out-of-bounds error. Or you check bounds after accessing the cell instead of before.
✅Always check bounds FIRST: if (nr >= 0 && nr < rows && nc >= 0 && nc < cols && grid[nr][nc] === target). Use the directions array pattern: const dirs = [[1,0],[-1,0],[0,1],[0,-1]] and loop through it. This is cleaner than writing 4 separate if-statements.
Using DFS for shortest path
You use DFS to find the shortest path in an unweighted graph. DFS finds A path, but not necessarily the shortest one. It might explore a long detour before finding the direct route.
✅For shortest path in unweighted graphs, ALWAYS use BFS. BFS explores by distance — the first time it reaches a node is guaranteed to be the shortest path. DFS has no such guarantee.
Building the adjacency list wrong
For undirected graphs, you add edge (u, v) but forget to add (v, u). Or for directed graphs, you add both directions when you should only add one. This corrupts the graph structure.
✅Undirected: add BOTH directions (u→v and v→u). Directed: add only the specified direction. Double-check by reading the problem statement: 'undirected' means bidirectional edges, 'directed' means one-way.
Forgetting disconnected components
You run BFS/DFS from node 0 and assume you've visited everything. But the graph might have multiple disconnected components. Nodes in other components are never visited.
✅Always loop over ALL nodes and start a traversal from each unvisited one. The pattern: for (let i = 0; i < n; i++) { if (!visited.has(i)) { bfs/dfs(i); componentCount++; } }. This handles disconnected graphs correctly.
Interview Strategy
Graph problems can feel overwhelming because there are many variations. Here's a structured approach to tackle them in interviews.
The 5-Step Interview Flow
Model the Graph
'What are the nodes? What are the edges? Is it directed or undirected? Can there be cycles?' — For grids: cells are nodes, adjacent cells are edges. For word problems: words are nodes, one-letter differences are edges. Explicitly state the graph model.
Choose BFS or DFS
'I need shortest path → BFS. I need to explore all paths or detect cycles → DFS. I need topological order → BFS (Kahn's) or DFS (post-order).' — Justify your choice. If either works (e.g., connected components), pick whichever you're more comfortable with.
Handle the Visited Strategy
'I'll use a visited set to avoid cycles. For grids, I'll mark cells in-place. For adjacency lists, I'll use a Set<number>.' — State this explicitly. Forgetting visited is the #1 graph bug.
Code It Clean
Build the adjacency list (if needed). Write the traversal using the appropriate template. Use the directions array for grids. Keep the code modular: separate graph building from traversal.
Analyze Complexity
'Time: O(V + E) — I visit every vertex and edge once. Space: O(V) for the visited set and queue/stack.' For grids: V = m×n, E = 4×m×n (each cell has at most 4 edges). Always mention both time and space.
Common Follow-Up Questions
| Follow-Up | How to Handle |
|---|---|
| "Can you do it with BFS instead of DFS?" (or vice versa) | Yes — for most problems, both work. BFS uses a queue, DFS uses recursion/stack. Mention the tradeoff: BFS guarantees shortest path; DFS uses less memory for deep narrow graphs. |
| "What if the graph is weighted?" | BFS no longer gives shortest path. Use Dijkstra's (non-negative weights) or Bellman-Ford (negative weights). Mention this upgrade path even if the current problem is unweighted. |
| "What if the grid is very large?" | DFS might cause stack overflow on huge grids (recursion depth = m×n). Use iterative DFS with an explicit stack, or BFS (which uses a queue instead of the call stack). |
| "How do you detect a cycle?" | Undirected: if a neighbor is visited and not the parent → cycle. Directed: use 3-color DFS (white/gray/black); gray→gray = back edge = cycle. Or use Kahn's: if not all nodes are processed, there's a cycle. |
| "Can you do it without extra space?" | For grids, modify the grid in-place (mark visited cells). For adjacency lists, you generally need O(V) space for the visited set — it's unavoidable for cycle prevention. |
| "What about bidirectional BFS?" | For shortest path between two specific nodes, BFS from both ends simultaneously. They meet in the middle, reducing the search space from O(b^d) to O(b^(d/2)). Useful for Word Ladder. |
The meta-skill
The hardest part of graph problems isn't the traversal — it's modeling the problem as a graph. Practice asking: "What are the nodes? What are the edges?" Once you have the graph model, the traversal is just a template. Word Ladder, Course Schedule, and Rotting Oranges all look different, but they're all just BFS/DFS on a graph with different node and edge definitions.
Summary & Cheat Sheet
Pin this section. Review it before mock interviews and contests.
Quick Revision Cheat Sheet
Core idea: BFS explores by distance (level by level, queue). DFS explores by depth (one branch fully, stack/recursion). Both visit every reachable node exactly once
BFS = shortest path: In unweighted graphs, BFS guarantees the shortest path. First visit = minimum distance. Always use BFS for 'minimum steps' problems
DFS = explore all: DFS is natural for path exploration, cycle detection, connected components, and topological sort. Recursive DFS is usually the cleanest code
Visited is mandatory: Graphs have cycles. Without a visited set, you loop forever. Mark visited WHEN ENQUEUING (BFS) or WHEN ENTERING (DFS)
Grid = implicit graph: Cells are nodes, adjacent cells are edges. Use dirs = [[1,0],[-1,0],[0,1],[0,-1]] for 4-directional movement
Multi-source BFS: Enqueue ALL sources at the start. Each BFS level = 1 time unit. Used for Rotting Oranges, 01 Matrix, Walls and Gates
Topological sort: Kahn's (BFS): process in-degree 0 nodes first. DFS: reverse post-order. Detects cycles: if not all nodes processed → cycle
Cycle detection: Undirected: visited neighbor ≠ parent → cycle. Directed: 3-color DFS, gray→gray = back edge = cycle
Complexity: Time: O(V + E). Space: O(V). For grids: V = m×n, E ≈ 4mn. Always state both
Build adjacency list: Undirected: add both u→v and v→u. Directed: add only the specified direction. Use Map<number, number[]>
Graph modeling: The hardest part. Ask: 'What are nodes? What are edges?' Word Ladder: words=nodes, 1-letter diff=edges. Course Schedule: courses=nodes, prereqs=edges
Mental trigger: "Shortest path" or "connected components" or "grid exploration" or "dependencies" → Graph Traversal (BFS or DFS)
Decision Flowchart
Can you model the problem as nodes and edges?
├── NO → Not a graph problem. Consider arrays, trees, or other patterns.
└── YES → Do you need the SHORTEST PATH (unweighted)?
├── YES → BFS with level tracking (Template 4)
│ Word Ladder, Shortest Path in Binary Matrix, Rotting Oranges
└── NO → Do you need TOPOLOGICAL ORDER (dependencies)?
├── YES → Kahn's BFS (in-degree) or DFS post-order
│ Course Schedule, Alien Dictionary
└── NO → Do you need to DETECT CYCLES?
├── YES → Undirected: DFS + parent check
│ Directed: 3-color DFS (gray→gray)
└── NO → Do you need CONNECTED COMPONENTS?
├── YES → DFS/BFS from each unvisited node
│ Number of Islands, Number of Provinces
└── NO → Is it a GRID problem?
├── YES → Grid DFS/BFS (Template 3)
│ Flood Fill, Surrounded Regions
└── NO → General BFS or DFS
Clone Graph, Is Bipartite
Graph Representation Quick Reference
| Representation | Space | Edge Lookup | When to Use |
|---|---|---|---|
| Adjacency List (Map) | O(V + E) | O(degree) | Sparse graphs (most interview problems) |
| Adjacency Matrix | O(V²) | O(1) | Dense graphs, need fast edge lookup |
| Edge List | O(E) | O(E) | Union-Find problems, Kruskal's MST |
| Implicit Grid | O(m × n) | O(1) | 2D grid problems (no explicit graph needed) |
One last thing
Graph traversals are the gateway to the most interesting problems in DSA: shortest paths, network flow, strongly connected components, and more. But at their core, BFS and DFS are simple — visit every reachable node exactly once, using a queue or a stack. The real skill is modeling the problem as a graph. Once you see the nodes and edges, the traversal is just a template. Practice the modeling step on every problem, and graph questions will stop feeling intimidating.