BFS vs DFS: Graph Traversal Algorithms Compared
Breadth-first search explores a graph level by level using a queue; depth-first search follows one path as far as it goes using a stack. When to use each.
Breadth-first search (BFS) and depth-first search (DFS) are the two fundamental strategies for visiting every reachable node in a graph or tree, and they differ in exactly one structural choice — BFS uses a queue and explores level by level outward from the start, while DFS uses a stack (often the call stack, via recursion) and follows a single path as deep as it can before backtracking. That one difference in data structure changes which problems each algorithm is naturally good at.
How BFS works
BFS starts at a source node, visits all of that node’s direct neighbors, then all of their unvisited neighbors, and so on — expanding outward in concentric rings. The mechanism is a first-in-first-out queue: pull a node off the front, mark it visited, push its unvisited neighbors onto the back, repeat.
BFS(start):
queue = [start]
visited = {start}
while queue is not empty:
node = queue.pop_front()
process(node)
for neighbor in node.neighbors:
if neighbor not in visited:
visited.add(neighbor)
queue.push_back(neighbor)
Because it expands one full layer at a time, BFS is the algorithm to reach for whenever you need the shortest path in terms of number of edges — the first time you reach a target node during a BFS is guaranteed to be via the fewest possible hops, as long as every edge has equal weight. That guarantee doesn’t hold once edges carry different weights or costs, which is the problem Dijkstra’s algorithm is built to solve instead.
How DFS works
DFS starts at a source node and immediately commits to one neighbor, then that neighbor’s neighbor, and keeps going deeper until it hits a dead end — a node with no unvisited neighbors — at which point it backtracks to the most recent branching point and tries the next unexplored path. The mechanism is a last-in-first-out stack, which is why DFS is usually written as recursion: each recursive call implicitly pushes a stack frame, and returning from it is the backtrack.
DFS(node, visited):
visited.add(node)
process(node)
for neighbor in node.neighbors:
if neighbor not in visited:
DFS(neighbor, visited)
DFS naturally produces an ordering of nodes based on when a path finishes exploring — the basis for topological sort on directed acyclic graphs, and for cycle detection, since revisiting a node that’s still “in progress” on the current path means you’ve found a cycle.
Side by side
| BFS | DFS | |
|---|---|---|
| Underlying structure | Queue (FIFO) | Stack (LIFO), often the call stack |
| Exploration pattern | Outward, level by level | Deep along one path, then backtrack |
| Finds shortest path (unweighted)? | Yes, guaranteed | No — may find a path, not the shortest |
| Memory use | Can be high — stores an entire frontier layer | Typically lower — stores one path at a time |
| Natural use cases | Shortest hop count, level-order traversal, nearest neighbors | Topological sort, cycle detection, exhaustive search/backtracking |
| Implementation style | Explicit queue, iterative | Recursive, or explicit stack |
Memory is the practical tradeoff
The table’s memory row is often the deciding factor in practice, not theoretical elegance. BFS has to hold an entire frontier — every node at the current distance from the start — in its queue before it can move to the next layer. On a graph that’s wide and shallow (a social network, where most people are only a few hops from anyone else), that frontier can be enormous. DFS only ever holds the nodes on its current path, which is bounded by the graph’s depth rather than its width, making it far cheaper on wide, shallow graphs.
The reverse is also true: on a graph that’s narrow and very deep, DFS’s recursion can blow the call stack, while BFS’s queue stays modest because each layer is small. Neither algorithm is unconditionally better — the graph’s shape determines which one’s memory profile is a problem.
Visited tracking is what makes both correct
Both algorithms rely on a visited set to avoid infinite loops on graphs with cycles — without it, both would happily bounce back and forth between two connected nodes forever. That visited set is almost always backed by a hash table for constant-time membership checks; using a plain list would turn every lookup into a linear scan and quietly wreck the algorithm’s runtime. On a tree, which by definition has no cycles, the visited set is technically optional, but it’s cheap insurance and standard practice anyway.
Where each shows up in real systems
BFS underlies web crawlers exploring pages level by level from a seed URL, “friends of friends” suggestions in social networks, and shortest-route logic in unweighted grids like puzzle solvers. It’s also the natural way to walk a linked list structure that branches, since level-order traversal of a tree is just BFS applied to a tree shape.
DFS underlies dependency resolution (build systems and package managers use topological sort, which is DFS-based), maze and puzzle solving where you need to explore one branch fully before giving up on it, and traversing nested structures like a filesystem tree or the node hierarchy behind a binary search tree. Compilers and static analyzers also lean on DFS for detecting cycles in dependency graphs, since an unexpected cycle there usually means a circular import or a broken build.
The takeaway
BFS and DFS visit the same graph, but the container each one uses — queue versus stack — changes what falls out for free. BFS’s queue guarantees the shortest path by edge count and suits wide, shallow graphs; DFS’s stack is memory-efficient on deep graphs and naturally produces the orderings that topological sort and cycle detection need. Neither is a strict upgrade over the other: pick based on whether you need shortest-hop guarantees or depth-first ordering, and keep an eye on which one your graph’s shape will make expensive.
Keep reading
The Lycoris Team · · 5 min read Kadane's Algorithm Explained: Maximum Subarray in O(n)
Kadane's algorithm finds the maximum-sum contiguous subarray in one O(n) pass. How it works, why it's correct, code, edge cases, and common variations.
The Lycoris Team · · 4 min read The Rope Data Structure: Editing Huge Strings Efficiently
A rope is a binary tree of string chunks that makes inserting, deleting, and slicing huge strings fast, which is why text editors use it over plain arrays.
The Lycoris Team · · 4 min read Memoization vs. Tabulation in Dynamic Programming
Memoization caches results top-down via recursion; tabulation builds a table bottom-up with loops. Same technique, opposite direction, different tradeoffs.