advanced23 min

Graphs: Representations, BFS, and DFS

Modeling relationships that don't fit a tree's strict hierarchy, and the two fundamental ways to systematically visit every reachable node.

What you'll learn

  • Represent a graph using an adjacency list
  • Implement breadth-first search and explain why it's the right tool for shortest-path-in-hops problems
  • Implement depth-first search, including cycle detection using a visited set

Prerequisites

Explanation

A graph generalizes a tree by dropping its strict hierarchy: nodes (vertices) connect via edges with no required parent/child structure, no "root," and — critically — cycles are allowed (a path that leads back to a node already visited), something a tree, by definition, can never have. A graph models any relationship that isn't naturally hierarchical: course prerequisites (which can have multiple valid paths converging), a social network, a road map, or a dependency graph between packages.

The standard, memory-efficient representation is an adjacency list: a map from each node to the list of nodes it directly connects to. { A: ["B", "C"], B: ["D"], C: ["D"], D: [] } represents a graph where A connects to B and C, both of which connect to D. This is dramatically more space-efficient than an adjacency matrix (an n×n grid marking every possible pair) for the sparse graphs — relatively few edges compared to the maximum possible — that most real-world graphs actually are.

Breadth-first search (BFS) explores level by level, using a queue: visit the start node, then every node one edge away, then every node two edges away, and so on. This level-by-level order is exactly what makes BFS the right tool whenever you need the shortest path measured in number of edges — the first time BFS reaches a target node is guaranteed to be via a shortest such path, because it's provably impossible for a node reached later, in a later "level," to be closer.

Depth-first search (DFS) explores as far as possible down one path before backtracking, using a stack (either explicit, or implicit via recursion — DFS is naturally recursive, in the same style as the tree traversals from two modules ago, since a tree is really just a graph with no cycles and exactly one path to every node). Both BFS and DFS require tracking a visited set — without one, a graph containing a cycle causes infinite re-visiting of the same nodes, which never happens in tree traversal precisely because trees can't have cycles; this is the one genuinely new bookkeeping requirement graphs introduce that trees never needed. The visited set doubles as straightforward cycle detection: encountering an already-visited node via an edge that isn't simply "back to where you immediately came from" (in an undirected graph) reveals a cycle.

Example

BFS (queue, level-by-level) and DFS (stack/recursion, depth-first) over the same adjacency-list graph, both tracking a visited set to handle cycles safely.

const graph = {
  A: ["B", "C"],
  B: ["A", "D"],
  C: ["A", "D"],
  D: ["B", "C", "E"],
  E: ["D"],
};

function bfs(graph, start) {
  const visited = new Set([start]);
  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 -- avoids duplicate enqueues
        queue.push(neighbor);
      }
    }
  }
  return order;
}

function dfs(graph, start, visited = new Set(), order = []) {
  visited.add(start);
  order.push(start);
  for (const neighbor of graph[start]) {
    if (!visited.has(neighbor)) {
      dfs(graph, neighbor, visited, order); // recursive call = implicit stack
    }
  }
  return order;
}

console.log(bfs(graph, "A")); // ["A", "B", "C", "D", "E"] -- level by level
console.log(dfs(graph, "A")); // ["A", "B", "D", "C", "E"] -- as deep as possible first

Try it yourself

Add a new node F connected only to E, and confirm both bfs and dfs from A eventually reach it.

Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.

Loading editor…

Guided exercise

Guided exercise

Write bfs(graph, start) returning the visit order (an array), correctly handling a graph with a cycle using a visited set (mark a node visited when it's ENQUEUED, not when dequeued, to avoid enqueuing the same node twice).

Checks: visits nodes in correct level order · terminates correctly and visits each node exactly once even with a cycle · handles a single isolated node with no edges

Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.

Loading editor…

Stuck? Get a hint.

Independent exercise

Independent exercise

Write hasPath(graph, start, end) using DFS (recursive, with a visited set) to determine whether ANY path exists from start to end (return true if start === end trivially). Then write hasCycle(graph) for an UNDIRECTED graph (every edge appears in both directions in the adjacency list) that detects whether any cycle exists, using DFS and tracking each node's parent to correctly ignore the trivial 'came right back the way I arrived' case.

Checks: finds a path that exists through an intermediate node · correctly reports no path to a disconnected node · a node trivially has a path to itself · correctly reports no cycle in a simple chain · correctly detects a genuine cycle (a triangle)

Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.

Loading editor…

Stuck? Get a hint.

Common mistakes

  • Forgetting the visited set entirely on a graph (unlike tree traversal, where it was never needed) -- a cycle then causes infinite re-visiting, which either hangs or eventually crashes with a stack overflow (for recursive DFS) or an ever-growing queue (for BFS).
  • Using DFS when the actual requirement is 'shortest path in number of edges' -- DFS finds A path, not necessarily the shortest one; only BFS's level-by-level order guarantees shortest-in-edges.
  • Detecting a cycle in an undirected graph without tracking the parent node -- since every edge is stored in both directions, the edge you just traversed always looks like 'a visited neighbor,' producing false positives unless the immediate parent is explicitly excluded from the check.

Knowledge check

Knowledge check

1. What genuinely new bookkeeping requirement do graphs introduce that tree traversal never needed?
2. Why does BFS, specifically, guarantee finding the shortest path measured in number of edges?
3. In cycle detection for an undirected graph, why must the DFS check exclude the immediate parent node when deciding whether a visited neighbor indicates a cycle?

Takeaway

Graphs generalize trees by allowing cycles and arbitrary connections, which makes a visited set mandatory bookkeeping BFS and DFS both need; BFS's level-by-level order specifically guarantees shortest-path-in-edges, while DFS explores depth-first and is the natural tool for reachability and cycle detection.

Summary

A graph is nodes connected by edges, with cycles allowed, typically represented as an adjacency list. BFS uses a queue and explores level by level, guaranteeing shortest path in edges. DFS uses a stack (or recursion) and explores depth-first. Both require a visited set to handle cycles safely, which trees never needed.

References

Your notes

Notes save automatically.