Graph Traversal: BFS and DFS Code Templates in 6 Languages
The concept guide covers when to reach for BFS versus DFS and why the visited check matters. This page gives you the code in a form you can memorize and adapt. The main template is BFS on an adjacency list, and every template below runs in
Main Template: BFS on an Adjacency List
This is the most common graph traversal template. It visits nodes in layers, and it is the right starting point for shortest path, level counting, and multi-source problems. The visited set does double duty. It prevents infinite loops in cyclic graphs and guarantees that each node is discovered exactly once.
Use this for Clone Graph and Rotting Oranges .
function bfs(graph, start) {
const queue = [start];
const visited = new Set([start]);
const result = [];
while (queue.length > 0) {
const node = queue.shift();
result.push(node);
// Mark neighbors on the way in. A node that is already
// marked was discovered on an earlier layer, so it is
// skipped and never enters the queue twice.
for (const neighbor of graph[node] || []) {
if (!visited.has(neighbor)) {
visited.add(neighbor);
queue.push(neighbor);
}
}
}
return result;
}Code Breakdown
Key Variables
queue: nodes waiting to be processed, in discovery order. This container is the only difference between BFS and DFS.visited: the set of nodes that have been enqueued. It prevents infinite loops and keeps every node in the queue exactly once.result: the traversal order. Some problems do not need this list, only the visits themselves.node: the current node. All the work for one step happens here. Record it, then look at its neighbors.
Visual Mechanism
stateDiagram-v2
[*] --> Init: enqueue start, mark visited
Init --> Loop: queue not empty
Loop --> Process: dequeue node
Process --> Neighbors: scan neighbor list
Neighbors --> Mark: neighbor unvisited
Mark --> Neighbors: mark and enqueue
Neighbors --> Loop: no unvisited neighbors left
Loop --> Done: queue empty
Done --> [*]
Critical Sections
The initialization is where the visited set is born. The start node is marked in the same step it is enqueued. Missing this one line turns a cyclic graph into an infinite loop, and it is the first thing interviewers check in a BFS.
The neighbor loop is where layers stay correct. Each neighbor is checked against the set, marked, and enqueued. Because marking happens before the node is processed, two neighbors of the same node cannot both enqueue it, and the queue only ever holds one copy of each node.
The termination condition is structural. The loop ends when the queue is empty, which means every node reachable from the start has been visited. Nodes outside the start’s component never appear, which is why connected-component problems wrap this loop in an outer loop over all nodes.
Variations
1. Recursive DFS
The same skeleton, but the call stack replaces the explicit stack. This is the template for connectivity, flood fill, and enumeration problems. The recursion depth is the risk. Use the iterative version when the graph can be deep.
Use this for Flood Fill and Pacific Atlantic Water Flow .
function dfs(graph, node, visited = new Set(), result = []) {
visited.add(node);
result.push(node);
// The call stack holds the search path. Backtracking
// happens automatically when a node runs out of neighbors
// and the function returns to its caller.
for (const neighbor of graph[node] || []) {
if (!visited.has(neighbor)) {
dfs(graph, neighbor, visited, result);
}
}
return result;
}2. Iterative DFS
The recursive version is cleaner, but an explicit stack is safer on deep graphs. Marking happens at push time. Marking at pop time would let the same node sit on the stack in several branches.
function dfsIterative(graph, start) {
const stack = [start];
const visited = new Set([start]);
const result = [];
while (stack.length > 0) {
const node = stack.pop();
result.push(node);
// Mark on push, not on pop. A node reachable from two
// branches would otherwise be pushed once per branch.
for (const neighbor of graph[node] || []) {
if (!visited.has(neighbor)) {
visited.add(neighbor);
stack.push(neighbor);
}
}
}
return result;
}3. Grid DFS (Flood Fill)
A grid is a graph where each cell has up to four neighbors. The only new work is the bounds check that runs before any access.
Use this for Flood Fill .
function floodFill(grid, sr, sc, newColor) {
const original = grid[sr][sc];
if (original === newColor) return grid;
const rows = grid.length;
const cols = grid[0].length;
function fill(r, c) {
// The bounds check must run before the value check.
// The other order reads outside the grid and crashes.
if (r < 0 || r >= rows || c < 0 || c >= cols || grid[r][c] !== original) return;
grid[r][c] = newColor;
fill(r + 1, c);
fill(r - 1, c);
fill(r, c + 1);
fill(r, c - 1);
}
fill(sr, sc);
return grid;
}4. BFS with Level Tracking
When a problem counts rounds or distances, drain the queue one layer at a time. The layer boundary is drawn by recording the queue size before processing.
Use this for Rotting Oranges .
function bfsLevels(graph, start) {
const queue = [start];
const visited = new Set([start]);
const levels = [];
while (queue.length > 0) {
// Record the size once. The for loop drains exactly
// the current layer, and everything pushed during the
// loop forms the next layer.
const size = queue.length;
const layer = [];
for (let i = 0; i < size; i++) {
const node = queue.shift();
layer.push(node);
for (const neighbor of graph[node] || []) {
if (!visited.has(neighbor)) {
visited.add(neighbor);
queue.push(neighbor);
}
}
}
levels.push(layer);
}
return levels;
}Now head to the practice problems to apply these templates to real interview questions.