Graphs · Graph Traversal Fundamentals

Graph BFS/DFS

Visiting every node reachable from a starting point, tracking what's already been visited.

  • 5 min read
  • 13 practice questions
  • 4 graded problems
On this page
  1. Description
  2. The approach
  3. A second flavor: BFS distances and the path itself
  4. Where you'll see it
  5. Which tool for which job?
  6. Common mistakes
  7. Check yourself
  8. Key takeaways
  9. Quick revise
  10. Practice

Description

Breadth-first search (BFS) and depth-first search (DFS) are the two ways to visit every node you can reach in a graph. Almost every graph question starts with one of them: is B reachable from A, how many separate groups are there, what's the fewest-hop route? A graph has no root and may have cycles or several separate pieces. A cycle leads back to a node you've already seen, so both traversals keep a visited set.

We'll go from simple to more technical:

  1. The pattern: DFS, BFS, and an outer loop that covers every component.
  2. A second flavor: BFS distances and rebuilding the path.
  3. Where traversals run in production (reachability, linked accounts, garbage collection).
  4. What breaks it, and how to choose between BFS and DFS.

The approach

Take six nodes with edges 0-1, 0-2, 1-3, 2-3 and 4-5. A BFS from 0 runs like this:

TakeNew neighbors (marked now)Queue after
01, 21, 2
13 (0 is already marked)2, 3
2none (0 and 3 are marked)3
3noneempty

Nodes 4 and 5 are never reached: they form a second component. An outer loop over every node starts a fresh traversal from 4, so the graph has 2 components.

Java

// DFS: go as deep as possible along one path, then back up
void dfs(int node, List<List<Integer>> adj, boolean[] visited) {
    visited[node] = true;
    for (int next : adj.get(node)) {
        if (!visited[next]) dfs(next, adj, visited);
    }
}

// BFS: visit everything 1 edge away, then everything 2 edges away, ...
void bfs(int start, List<List<Integer>> adj, boolean[] visited) {
    Deque<Integer> queue = new ArrayDeque<>();
    visited[start] = true;                     // mark on discovery...
    queue.add(start);
    while (!queue.isEmpty()) {
        int node = queue.poll();
        for (int next : adj.get(node)) {
            if (!visited[next]) {
                visited[next] = true;          // ...not when it's taken out of the queue
                queue.add(next);
            }
        }
    }
}

// Either traversal reaches one component; the outer loop covers them all
int components = 0;
for (int v = 0; v < n; v++) {
    if (!visited[v]) { components++; dfs(v, adj, visited); }
}

Python

from collections import deque


# DFS: go as deep as possible along one path, then back up
def dfs(node: int, adj: list[list[int]], visited: list[bool]) -> None:
    visited[node] = True
    for nxt in adj[node]:
        if not visited[nxt]:
            dfs(nxt, adj, visited)


# BFS: visit everything 1 edge away, then everything 2 edges away, ...
def bfs(start: int, adj: list[list[int]], visited: list[bool]) -> None:
    queue = deque([start])
    visited[start] = True                  # mark on discovery...
    while queue:
        node = queue.popleft()
        for nxt in adj[node]:
            if not visited[nxt]:
                visited[nxt] = True        # ...not when it's taken out of the queue
                queue.append(nxt)


# Either traversal reaches one component; the outer loop covers them all
components = 0
for v in range(n):
    if not visited[v]:
        components += 1
        dfs(v, adj, visited)

Each node is expanded once, so each edge is looked at a constant number of times: O(V + E) with adjacency lists. Each fresh start in the outer loop explores one component, so counting fresh starts counts components.

A second flavor: BFS distances and the path itself

BFS finishes every node at distance d before it touches distance d + 1. So when it first discovers a node, it has found a fewest-edge path to it. In the graph above, 1 and 2 are at distance 1 and 3 is at distance 2. Record the distance and the node you came from at discovery, and you can rebuild the path:

Java

int[] dist = new int[n], parent = new int[n];
Arrays.fill(dist, -1);                         // -1 = not discovered; doubles as visited
Arrays.fill(parent, -1);
dist[src] = 0;
queue.add(src);
while (!queue.isEmpty()) {
    int u = queue.poll();
    for (int v : adj.get(u)) {
        if (dist[v] == -1) { dist[v] = dist[u] + 1; parent[v] = u; queue.add(v); }
    }
}
// path: walk parent[] back from dst to src, then reverse

Python

dist = [-1] * n                            # -1 = not discovered; doubles as visited
parent = [-1] * n
dist[src] = 0
queue = deque([src])
while queue:
    u = queue.popleft()
    for v in adj[u]:
        if dist[v] == -1:
            dist[v] = dist[u] + 1
            parent[v] = u
            queue.append(v)
# path: walk parent[] back from dst to src, then reverse

DFS gives no such guarantee: its first path to a node can be long. With weighted edges, fewest edges isn't cheapest either. That's Dijkstra's job.

Where you'll see it

Reachability, connected groups (accounts that share a device) and fewest-hop routes (a crawler's link depth) are all BFS or DFS. Garbage collectors find live objects the same way, marking everything reachable from the roots.

Which tool for which job?

You needUse
Is B reachable from A?either traversal from A
Count or label connected groupsan outer loop over unvisited nodes, one traversal each
Fewest edges between two nodesBFS, recording distance and parent at discovery
Paths, cycles or an orderingDFS
Weighted shortest pathsDijkstra, not BFS

Common mistakes

MistakeWhat happensDo this instead
No visited checkloops forever on a cyclemark every node you reach
Marking on dequeue, not on enqueue19,901 queue insertions instead of 200 on a complete graphmark when you enqueue
One traversal for the whole graphother components are silently skippedan outer loop over all nodes
Recursive DFS on a deep graphstack overflow (Python at about 1,000 frames)an explicit stack
list.pop(0) as a Python queueBFS turns quadraticcollections.deque and popleft()
DFS for shortest pathsfinds a path, not the fewest-edge oneBFS

Check yourself

Try to answer before opening each one - recalling beats re-reading.

1. A BFS marks a node visited only when it's dequeued (and skips it if it was already visited). Is the result wrong, and what does it cost?

Show answer

The order still comes out right. But until a node is dequeued, every edge into it adds another copy: 19,901 insertions instead of 200 on a complete graph of 200 nodes. Marking on enqueue keeps each node in the queue once.

2. Which uses less memory, DFS or BFS?

Show answer

It depends on the shape. DFS keeps the current path, so a long chain costs O(V). BFS keeps the frontier, so a star with one hub costs O(V), where DFS needs two frames. Both are O(V) in the worst case.

3. How do you count connected components with a traversal?

Show answer

Loop over every node, and start a fresh traversal from each one still unvisited, adding one to the count. The traversal marks what it reaches, so nothing is counted twice.

Key takeaways

  • Every traversal needs a visited set, and marks a node when it first discovers it.
  • One traversal covers one component; an outer loop over unvisited nodes covers the rest, and counts them.
  • BFS discovers nodes in order of edge distance, so dist and parent recorded at discovery give fewest-edge paths.
  • Both are O(V + E) time and O(V) memory in the worst case.

Quick revise

The whole skill on one page, for the night before an interview.

This page puts the lesson and the deep dive together. Skim it before practice or an interview, and go back to the full lesson for anything you don't recognize.

Pick the tool

You needUse
Reachabilityeither traversal
Componentsouter loop plus a traversal
Fewest edgesBFS with dist and parent
Cycles, order, pathsDFS
Deep graphsexplicit stack (mark on pop for true DFS order)
Fast point-to-point hopsbidirectional BFS

Snippets worth knowing by heart

Java

int[] dist = new int[n];
Arrays.fill(dist, -1);
dist[src] = 0;
Deque<Integer> q = new ArrayDeque<>(List.of(src));
while (!q.isEmpty()) {
    int u = q.poll();
    for (int v : adj.get(u)) if (dist[v] == -1) { dist[v] = dist[u] + 1; q.add(v); }
}

Python

seen, stack = set(), [start]             # iterative DFS, true DFS order
while stack:
    u = stack.pop()
    if u in seen:
        continue
    seen.add(u)
    for v in reversed(adj[u]):
        stack.append(v)

The traps

  • No visited set: the traversal loops forever on a cycle. Mark every node you reach.
  • Marking on dequeue: a node is queued once per neighbor. Mark it when you enqueue it.
  • No outer loop over the nodes: other components are silently skipped. Start a traversal from every unvisited node.
  • Recursion depth: Python stops near 1,000 frames. Use an explicit stack on deep graphs.
  • list.pop(0) as a queue: each pop shifts the list, so BFS turns quadratic. Use collections.deque.
  • DFS for shortest paths: it finds a path, not the fewest-edge one. Use BFS.

Staff-level points, one line each

  • Recursive DFS overflowed Java's stack on a 100,000-node path; the explicit stack reached all 100,000.
  • Mark on push changed the DFS order (0, 1, 2, 3 instead of 0, 1, 3, 2); mark on pop matched the recursive order.
  • Marking on dequeue: 19,901 queue insertions instead of 200 on a complete graph of 200 nodes.
  • CSR used under a third of a list of lists' memory (100,000 nodes, 1,000,000 entries).
  • Bidirectional BFS: 1,195 nodes discovered per query against 86,062 one-sided.
  • list.pop(0) BFS on a 100,000-node star was at least 10x slower than deque.

Say it in an interview

  • "Both are O(V + E) with adjacency lists; I mark nodes when I discover them."
  • "BFS gives fewest edges, so I record distance and parent at discovery."
  • "For deep graphs I use an explicit stack, marking on pop if I need DFS order."
  • "Point to point on a big graph, I'd search from both ends."

Practice

This skill comes with 13 multiple-choice questions that tell you which misconception a wrong answer reveals and 4 graded problems. Your answers are graded and your mastery is tracked, so you'll know when the skill holds up. Running code and saving progress need a free account.

Practice Graph BFS/DFS free

BFS Traversal of Graph

EasyFreeCoding problemJava · Python

Given an undirected graph with n nodes (labeled 0 to n-1) and a list of edges, return the BFS traversal order, visiting nodes' neighbors in ascending order and starting a new traversal from the smallest unvisited node whenever the graph is disconnected.

DFS Traversal of Graph

EasyFreeCoding problemJava · Python

Given an undirected graph with n nodes (labeled 0 to n-1) and a list of edges, return the DFS traversal order, visiting each node's neighbors in ascending order and starting a new traversal from the smallest unvisited node whenever the graph is disconnected.

Count Connected Components in an Undirected Graph

MediumProCoding problemJava · Python

You have n nodes labeled 0 to n-1, and an undirected graph given as a list of edges. Return the number of connected components.

The full statement, the examples and the hidden test suite are part of AI Proof Engineers Pro.

Shortest Path in an Unweighted Graph

MediumProCoding problemJava · Python

You have n nodes labeled 0 to n-1 and an undirected, unweighted graph given as a list of edges. Given a source node src and a destination node dst, return the length (number of edges) of the shortest path between them, or -1 if dst is unreachable from src.

The full statement, the examples and the hidden test suite are part of AI Proof Engineers Pro.

Practice Graph BFS/DFS free

No card needed. Lessons, practice questions and easy problems are free forever.

Free plan, no card · 7-day Pro trial · Cancel anytime