DAY 23 / 30 · WEEK 4

Graphs — BFS & DFS

Goal of the day

Concept

A tree (Day 19) is actually a special, restricted kind of graph: no cycles, and exactly one path between any two nodes. That's precisely why tree BFS/DFS never needed to track visited nodes — walking "down" from a parent to its children could never lead back to a node you'd already seen, because there's no path back up except the one you just came from.

A general graph drops that restriction. Nodes can have multiple neighbors that lead to each other in loops — cycles — and there can be more than one path between two nodes. Think of a road map: City A connects to City B, City B connects to City C, and City C connects right back to City A. Or a social network: you're friends with Sam, Sam is friends with Priya, and Priya is friends with you too. Follow those connections without remembering where you've been, and you'll walk A → B → C → A → B → C forever — the traversal never terminates.

That's the one genuinely new idea in this lesson: a visited set (a Set or a plain object used as a hash set) that records every node the instant it's been discovered, so BFS/DFS never re-processes — and never re-queues or re-recurses into — a node twice. Everything else (the queue for BFS, the call stack or recursion for DFS) is exactly what you already know from Day 19; the visited set is the only new moving part, and it's the difference between a traversal that finishes and one that hangs forever.

Diagram: graph structure and its adjacency list

7 nodes, one cycle (A-B-D-C-A) — adjacency list on the right A B C D E F G Adjacency list (undirected): A: [B, C] B: [A, D] C: [A, D] D: [B, C, E, G] E: [D, F] F: [E] G: [D] Each undirected edge is stored twice — once in each endpoint's list. This is exactly the representation BFS and DFS will walk in the code below.

Diagram: why a cycle needs a visited set

A pure 3-node cycle: X - Y - Z - X X Y Z Without a visited set: X → Y → Z → X → Y → Z → ⋯ (never stops) — the walk keeps re-visiting the same 3 nodes forever. With a visited set: mark each node visited on first arrival — X → Y → Z, then stop. All 3 nodes visited exactly once.

Interactive visualization: BFS traversal

in the queue (discovered, waiting) dequeued, checking neighbors now fully processed
Press Play or Step to begin.

Interactive visualization: DFS traversal

on the call stack (ancestor, waiting) current call, checking neighbors now finished, returned
Press Play or Step to begin.

The code: BFS and DFS on an adjacency list

function bfs(graph, start) {
  const visited = new Set([start]);   // mark the start visited immediately
  const queue = [start];
  const order = [];

  while (queue.length > 0) {
    const node = queue.shift();
    order.push(node);
    for (const neighbor of graph[node]) {
      if (!visited.has(neighbor)) {
        visited.add(neighbor);        // mark visited when ENQUEUED, not when dequeued
        queue.push(neighbor);
      }
    }
  }
  return order;
}

function dfs(graph, start, visited = new Set(), order = []) {
  visited.add(start);                  // mark visited the instant we enter
  order.push(start);
  for (const neighbor of graph[start]) {
    if (!visited.has(neighbor)) {
      dfs(graph, neighbor, visited, order);
    }
  }
  return order;
}

Line by line:

Dry run: BFS from A

Queue (before)DequeueNeighbor checksVisited set (after)
[A]AB new → enqueue; C new → enqueue{A, B, C}
[B, C]BA visited, skip; D new → enqueue{A, B, C, D}
[C, D]CA visited, skip; D visited, skip{A, B, C, D}
[D]DB, C visited, skip; E new → enqueue; G new → enqueue{A, B, C, D, E, G}
[E, G]ED visited, skip; F new → enqueue{A, B, C, D, E, F, G}
[G, F]GD visited, skip{A, B, C, D, E, F, G}
[F]FE visited, skip{A, B, C, D, E, F, G}

BFS order: A → B → C → D → E → G → F. Notice C and D both get checked as neighbors of two different nodes (B and D check each other's territory) — without the visited set, C would enqueue D a second time, and the 4-node cycle A-B-D-C-A would never let the queue empty out.

Dry run: DFS from A

Call stack (path from A)ActionVisited set (after)
[A]Visit A (start){A}
[A, B]A → B unvisited, recurse{A, B}
[A, B, D]B → A visited skip; B → D unvisited, recurse{A, B, D}
[A, B, D, C]D → B, C: C unvisited, recurse (B was skipped){A, B, D, C}
[A, B, D]C → A, D both visited — backtrack out of C{A, B, D, C}
[A, B, D, E]D → C visited skip; D → E unvisited, recurse{A, B, D, C, E}
[A, B, D, E, F]E → D visited skip; E → F unvisited, recurse{..., F}
[A, B, D, E] → [A, B, D]F → E visited — backtrack out of F, then out of E{..., F}
[A, B, D, G]D → G unvisited, recurse{..., G}
[] (empty)G, D, B, A all exhausted — backtrack all the way out{A, B, C, D, E, F, G}

DFS order: A → B → D → C → E → F → G — a genuinely different order from BFS on the exact same graph, because DFS commits to one neighbor's whole subtree before backtracking to try the next one.

Complexity

OperationTimeSpaceWhy
BFS traversalO(V + E)O(V)The visited set guarantees every vertex is enqueued and processed exactly once — O(V). Every edge is examined once from each endpoint it touches, so O(E) for undirected (O(2E), same order of growth) or O(E) for directed.
DFS traversalO(V + E)O(V)Same reasoning as BFS — the visited set bounds vertex visits to V, edge checks to E. Space is the recursion depth (or explicit stack size), worst case O(V) for a graph that's one long path.
Adjacency list (representation)O(1) average to list a node's neighborsO(V + E)Most common in interviews — compact for sparse graphs, and it's exactly what BFS/DFS above iterate over.
Adjacency matrix (representation)O(1) to check if a specific edge existsO(V squared)Simple lookups for "are u and v connected", but wastes space on sparse graphs — a graph with 1000 nodes and 2000 edges still allocates 1,000,000 matrix cells.

Interview corner

How to talk through it out loud: name the representation first — "I'll model this as an adjacency list, node to array of neighbors" — then name the traversal and its engine — "BFS with a queue" or "DFS with recursion" — and immediately say the visited-set rule out loud: "I'll mark each node visited the moment I discover it, not when I finish processing it, so I never enqueue or recurse into the same node twice even if the graph has cycles." Interviewers on a graph problem are listening specifically for whether you treat the visited set as a first-class, load-bearing part of the algorithm — not an afterthought bolted on after your code infinite-loops on their test case.

Practice problems

1. Find if Path Exists in Graph (Easy — LeetCode: Find if Path Exists in Graph)

Given a graph as an edge list, a source, and a destination, return whether there's a path from source to destination. Today's traversal, with an early exit.

2. Number of Islands (Medium — LeetCode: Number of Islands)

Given a 2D grid of '1' (land) and '0' (water), count the number of islands — a grid is a graph in disguise, where each cell is a node connected to its up / down / left / right neighbors.

3. Clone Graph (Medium — LeetCode: Clone Graph)

Given a reference node in a connected undirected graph, return a deep copy of the whole graph. The graph can contain cycles, so the visited set has to double as a map from original node to its clone.

Quiz

1. Why does graph traversal need a visited set when Day 19's tree traversal never did?

2. In BFS, when should a node be added to the visited set?

3. How do you make sure every node gets visited in a disconnected graph?

4. What is the time complexity of BFS or DFS on a graph, and why?

5. For a typical sparse interview graph, which representation is the usual default, and why?