BFS (breadth-first search) explores a graph level by level using a queue, so it finds the shortest path in an unweighted graph. DFS (depth-first search) follows one branch as deep as it can before backtracking, using a stack or recursion, which suits cycle detection, topological sorting and backtracking puzzles. Both visit every node and edge once: O(V + E) time.
Tested with: Node.js 20.20 and Python 3.14, October 2026. All outputs and timings below come from those runs.
BFS vs DFS at a Glance
| BFS | DFS | |
|---|---|---|
| Order | Level by level (closest nodes first) | One branch to the end, then backtrack |
| Data structure | Queue (first in, first out) | Stack (last in, first out) or recursion |
| Shortest path (unweighted) | Yes, guaranteed | No |
| Time | O(V + E) | O(V + E) |
| Extra memory | O(V); in practice the widest level | O(V); in practice the deepest path |
| Good for | Shortest path, nearest match, levels, friend-of-friend | Cycles, topological sort, connected components, mazes, backtracking |
| Watch out for | Memory on very wide graphs | Stack overflow when recursion is deep |
V is the number of nodes (vertices) and E the number of edges. New to graphs? Start with our graph data structures guide.
The Example Graph
// Adjacency list: each node maps to the nodes it points to
export const graph = {
A: ["B", "C"],
B: ["D", "E"],
C: ["F"],
D: [],
E: ["F"],
F: [],
}; A
/ \
B C
/ \ \
D E → F (E and C both point to F)
BFS from A: A B C D E F (level by level)
DFS from A: A B D E F C (one branch to the bottom, then back up)Breadth-First Search (BFS)
Take the first node from the queue, record it, and add its unvisited neighbors to the back. Mark a node visited when you add it to the queue, not when you take it out:
// Breadth-first search: visit nodes level by level
export function bfs(graph, start) {
const visited = new Set([start]);
const queue = [start];
const order = [];
// Read with an index instead of queue.shift(), which is O(n) per call
for (let head = 0; head < queue.length; head++) {
const node = queue[head];
order.push(node);
for (const next of graph[node] ?? []) {
if (!visited.has(next)) {
visited.add(next); // mark when queued, so nothing is queued twice
queue.push(next);
}
}
}
return order;
}import { graph } from "./graph.js";
import { bfs } from "./bfs.js";
console.log(bfs(graph, "A")); // ["A", "B", "C", "D", "E", "F"]Two bugs in the usual BFS
// A common version (including an older copy of this article):
while (queue.length > 0) {
const node = queue.shift(); // O(n): moves every remaining item
if (!visited.has(node)) {
visited.add(node); // marked only when dequeued...
queue.push(...graph[node]); // ...so F gets queued twice
}
}- Marking on dequeue queues nodes twice. On this 6-node graph the old version queued 7 nodes (F twice). On dense graphs the waste is much larger.
- queue.shift() is slow. It moves every remaining item. On a node with 100,000 neighbors, the shift() version took 933 ms; the head-index version took 38 ms (20,000: 35 ms vs 8 ms).
- push(...array) can crash. Spreading 200,000 neighbors into push() threw RangeError: Maximum call stack size exceeded, because each item becomes a function argument. A loop doesn't have that limit.
Shortest path with BFS
Because BFS reaches nodes in order of distance, the first time it reaches the goal is via a shortest path. Remember each node's parent to rebuild it:
// Fewest-edges path from start to goal (unweighted graph), or null
export function shortestPath(graph, start, goal) {
const parent = new Map([[start, null]]);
const queue = [start];
for (let head = 0; head < queue.length; head++) {
const node = queue[head];
if (node === goal) {
const path = [];
for (let at = goal; at !== null; at = parent.get(at)) path.push(at);
return path.reverse();
}
for (const next of graph[node] ?? []) {
if (!parent.has(next)) {
parent.set(next, node);
queue.push(next);
}
}
}
return null; // goal can't be reached
}shortestPath(graph, "A", "F"); // ["A", "C", "F"] (2 edges; A→B→E→F is 3)
shortestPath(graph, "D", "A"); // null (edges only go one way)This counts edges, not distances. When edges have weights (road lengths, costs), use Dijkstra's algorithm, or Bellman-Ford if weights can be negative.
BFS on a grid: fewest steps through a maze
// Fewest steps through a maze: "#" is a wall, moves are up/down/left/right
export function mazeSteps(maze, [startRow, startCol], [endRow, endCol]) {
const rows = maze.length;
const cols = maze[0].length;
const seen = new Set([`${startRow},${startCol}`]);
const queue = [[startRow, startCol, 0]];
for (let head = 0; head < queue.length; head++) {
const [r, c, steps] = queue[head];
if (r === endRow && c === endCol) return steps;
for (const [dr, dc] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) {
const nr = r + dr;
const nc = c + dc;
const key = `${nr},${nc}`;
if (nr < 0 || nc < 0 || nr >= rows || nc >= cols) continue;
if (maze[nr][nc] === "#" || seen.has(key)) continue;
seen.add(key);
queue.push([nr, nc, steps + 1]);
}
}
return -1; // no way through
}const maze = [
"S..#....",
".#.#.##.",
".#...#..",
".####.#.",
"......#E",
];
mazeSteps(maze, [0, 0], [4, 7]); // 15
mazeSteps(["S#", "#E"], [0, 0], [1, 1]); // -1 (walled off)Grids are graphs too: each open cell is a node and its four neighbors are edges. This is the pattern behind many "minimum moves" interview questions.
Depth-First Search (DFS)
The recursive version is the easiest to read. The stack version does the same work without using the call stack:
// Depth-first search, recursive: short and clear, but limited by the call stack
export function dfsRecursive(graph, node, visited = new Set(), order = []) {
visited.add(node);
order.push(node);
for (const next of graph[node] ?? []) {
if (!visited.has(next)) dfsRecursive(graph, next, visited, order);
}
return order;
}
// Depth-first search with an explicit stack: same order, no recursion limit
export function dfs(graph, start) {
const visited = new Set();
const stack = [start];
const order = [];
while (stack.length > 0) {
const node = stack.pop();
if (visited.has(node)) continue;
visited.add(node);
order.push(node);
// Push in reverse so the first neighbor is explored first
const neighbors = graph[node] ?? [];
for (let i = neighbors.length - 1; i >= 0; i--) {
if (!visited.has(neighbors[i])) stack.push(neighbors[i]);
}
}
return order;
}dfs(graph, "A"); // ["A", "B", "D", "E", "F", "C"]
dfsRecursive(graph, "A"); // ["A", "B", "D", "E", "F", "C"]Both returned the same order in our test. The stack version pushes neighbors in reverse so the first neighbor is explored first; without that, it still works but visits branches right to left.
Recursion depth limits
- Node.js allowed about 11,400 nested calls in our test. Recursive DFS on a 100,000-node chain threw RangeError: Maximum call stack size exceeded; the stack version visited all 100,000 nodes.
- Python's default limit is 1,000. A 5,000-node chain raised RecursionError: maximum recursion depth exceeded; the iterative version worked.
Use recursion for trees and small graphs; switch to an explicit stack when depth can grow with the input. More in our recursion guide.
Cycle detection with DFS
In a directed graph, a cycle exists if DFS reaches a node that is still on the current path:
// Cycle detection in a directed graph with DFS.
// "visiting" = on the current path; reaching it again means a cycle.
export function hasCycle(graph) {
const state = new Map(); // node -> "visiting" | "done"
function visit(node) {
if (state.get(node) === "visiting") return true;
if (state.get(node) === "done") return false;
state.set(node, "visiting");
for (const next of graph[node] ?? []) {
if (visit(next)) return true;
}
state.set(node, "done");
return false;
}
return Object.keys(graph).some((node) => visit(node));
}hasCycle(graph); // false
hasCycle({ ...graph, F: ["B"] }); // true: B → E → F → BA plain "visited" set isn't enough for directed graphs: in our example graph F is reached twice (via C and via E) without any cycle. The "visiting" state is what tells the two cases apart. The same DFS, recording nodes as they finish, gives a topological order (for build steps, course prerequisites or task scheduling).
BFS and DFS in Python
from collections import deque
graph = {
"A": ["B", "C"],
"B": ["D", "E"],
"C": ["F"],
"D": [],
"E": ["F"],
"F": [],
}
def bfs(graph, start):
visited = {start}
queue = deque([start]) # popleft() is O(1); list.pop(0) is O(n)
order = []
while queue:
node = queue.popleft()
order.append(node)
for nxt in graph.get(node, []):
if nxt not in visited:
visited.add(nxt)
queue.append(nxt)
return order
def dfs(graph, start):
visited = set()
stack = [start]
order = []
while stack:
node = stack.pop()
if node in visited:
continue
visited.add(node)
order.append(node)
for nxt in reversed(graph.get(node, [])):
if nxt not in visited:
stack.append(nxt)
return order
def dfs_recursive(graph, node, visited=None):
if visited is None: # never use visited=set() as a default
visited = set()
visited.add(node)
order = [node]
for nxt in graph.get(node, []):
if nxt not in visited:
order += dfs_recursive(graph, nxt, visited)
return order
print(bfs(graph, "A")) # ['A', 'B', 'C', 'D', 'E', 'F']
print(dfs(graph, "A")) # ['A', 'B', 'D', 'E', 'F', 'C']
print(dfs_recursive(graph, "A")) # ['A', 'B', 'D', 'E', 'F', 'C']All three printed the orders shown in the comments. Use collections.deque for the queue: list.pop(0) has the same O(n) problem as shift().
# Don't do this: the default set is created once and shared by every call
def dfs(graph, node, visited=set()):
...
dfs(graph, "A") # prints A B D E F C
dfs(graph, "A") # prints nothing: every node is already "visited"We ran this: the second call printed nothing. Python evaluates default arguments once, so every call shares the same set. Use visited=None as in dfs_recursive above.
When to Use BFS vs DFS
| Problem | Use | Why |
|---|---|---|
| Shortest path / fewest moves (unweighted) | BFS | Reaches nodes in order of distance |
| People within 2 connections, nearby items | BFS | Stops after the levels you need |
| Web crawler limited to N clicks from the homepage | BFS | Crawls by depth level |
| Is there any path / are these connected? | Either | Both visit everything reachable |
| Cycle detection, dependency order | DFS | Tracks the current path |
| Sudoku, N-Queens, permutations | DFS (backtracking) | Tries one choice fully, then undoes it |
| Very deep graph | Iterative DFS or BFS | Recursion would overflow |
| Very wide graph, answer is deep | DFS | BFS would hold a huge level in memory |
Backtracking is DFS over choices; see backtracking with N-Queens and Sudoku. For minimum spanning trees, see Kruskal's algorithm.
BFS and DFS in AI (Search Problems)
In AI courses, BFS and DFS are uninformed search strategies: they explore a state space (puzzle positions, game states, routes) without a heuristic. With branching factor b, solution depth d and maximum depth m:
| BFS | DFS | Iterative deepening DFS | |
|---|---|---|---|
| Complete (always finds a solution if one exists) | Yes (finite b) | No (can go down an infinite branch) | Yes |
| Optimal (fewest steps) | Yes, if every step costs the same | No | Yes, if every step costs the same |
| Time | O(b^d) | O(b^m) | O(b^d) |
| Memory | O(b^d) | O(b·m) | O(b·d) |
Iterative deepening runs DFS with a depth limit of 1, 2, 3… It keeps DFS's small memory and BFS's guarantee of the shallowest solution, which is why it's the usual choice for large search spaces. When you have a good estimate of the distance to the goal, informed search such as A* beats all three. See Big-O notation if the complexity notation is new.
Common Mistakes
| Mistake | What happens | Fix |
|---|---|---|
| Marking visited when dequeuing | Nodes queued several times | Mark when you enqueue |
| queue.shift() / list.pop(0) | O(n²) on big graphs | Head index, or deque in Python |
| queue.push(...neighbors) | RangeError with very many neighbors | Push in a loop |
| Recursive DFS on deep input | Stack overflow / RecursionError | Explicit stack |
| visited=set() default in Python | Second call returns nothing | visited=None, create inside |
| Using BFS for weighted shortest paths | Fewest edges, not lowest cost | Dijkstra |
| Plain visited set for directed cycles | False positives (F reached twice) | Track nodes on the current path |
FAQs
What is the main difference between BFS and DFS?
BFS visits nodes in order of distance from the start, using a queue. DFS goes as deep as possible along one branch before backtracking, using a stack or recursion.
Which is faster, BFS or DFS?
Both are O(V + E) when implemented correctly. Which one finds your answer sooner depends on where it is: near the start favors BFS, deep in one branch favors DFS.
Why does BFS find the shortest path?
It processes every node at distance 1 before any at distance 2, and so on. The first time it reaches the goal, no shorter path can exist. This holds only when all edges count the same.
Which uses more memory?
BFS stores a whole level at once, so it uses more on wide graphs. DFS stores the current path, so it uses more on long, deep graphs. Worst case, both are O(V).
Is DFS always recursive?
No. Any recursive DFS can use an explicit stack instead, which avoids stack-overflow errors on deep graphs.
What are BFS and DFS used for in real life?
BFS: route finding with equal step costs, social "people you may know", network broadcast, level-limited crawling. DFS: dependency resolution and build order, detecting deadlocks (cycles), maze generation, solving puzzles.
Related Articles
View allAbout Muhaymin Bin Mehmood
Front-end Developer skilled in the MERN stack, experienced in web and mobile development. Proficient in React.js, Node.js, and Express.js, with a focus on client interactions, sales support, and high-performance applications.




