Breadth-first search, depth-first search, and topological sort
Lucas P. Cordova, Ph.D.
Willamette University | Fall 2026
Given one vertex of a graph, which vertices can we reach from it, how few edges does each take, and in what order should we visit them?
Vocabulary, representation, and the questions we ask about paths
THE RUNNING EXAMPLE: 8 VERTICES, 8 EDGES, UNDIRECTED. H IS ON ITS OWN.
Adj(A) = {B, C}, deg(A) = 2. Adj(F) = {D, E, G}, deg(F) = 3.
1 G = { 2 'A': ['B', 'C'], 3 'B': ['A', 'D'], 4 'C': ['A', 'D', 'E'], 5 'D': ['B', 'C', 'F'], 6 'E': ['C', 'F'], 7 'F': ['D', 'E', 'G'], 8 'G': ['F'], 9 'H': [], 10 }
EVERY UNDIRECTED EDGE APPEARS IN TWO LISTS.
δ(s, v) is the length of a shortest path from s to v (the fewest edges). If no path exists, δ(s, v) = ∞.
δ(A, v) FOR EVERY VERTEX v. H IS UNREACHABLE.
Two dictionaries: d (the distances) and parent (the tree). Everything else is derived from those.
TEAL: THE SHORTEST-PATH TREE FROM A. parent[G] = F, parent[F] = D, parent[D] = B, parent[B] = A.
Explore in order of distance: level by level
BUILDING L3 FROM L2 = {D, E}: THE AMBER EDGES ARE THE ONES BEING CHECKED.
| level | vertices (d = level) |
|---|---|
| L0 | A |
| L1 | B, C |
| L2 | D, E |
MAGENTA: THE LEVEL JUST BUILT.
| level | vertices (d = level) |
|---|---|
| L0 | A |
| L1 | B, C |
| L2 | D, E |
| L3 | F |
| L4 | G |
THE FINISHED SEARCH: FIVE LEVELS, ONE UNREACHABLE VERTEX.
1 def bfs(adj, s): 2 d, parent = {s: 0}, {s: None} 3 queue = [s] # FIFO 4 while queue: 5 u = queue.pop(0) # dequeue 6 for v in adj[u]: 7 if v not in d: # not seen yet 8 d[v] = d[u] + 1 9 parent[v] = u 10 queue.append(v) 11 return d, parent
v not in d is the seen check: d doubles as the visited set.collections.deque; list.pop(0) is O(n).d[v] = δ(s, v) for every v, and parent forms a shortest-path tree, in O(|V| + |E|) time. It works on directed and undirected graphs alike.
Follow a path until stuck, then back up
for each v in Adj(u): if v has no parent yet, set parent[v] = u and visit(v). When the loop ends, u is finished.
THE DFS TREE FROM A. t = DISCOVERY TIME (THE CLOCK ALSO TICKS AT EACH FINISH). THE PATH TO G HAS 6 EDGES; BFS FOUND ONE WITH 4.
Discovery order
A B D C E F GFinishing order
G F E C D B ANEIGHBORS ARE ALWAYS TAKEN IN ALPHABETICAL ORDER.
1 def dfs(adj, s): 2 parent = {s: None} 3 finished = [] 4 def visit(u): 5 for v in adj[u]: 6 if v not in parent: 7 parent[v] = u 8 visit(v) 9 finished.append(u) # u is done 10 visit(s) 11 return parent, finished
SECOND RUN: visit(H) FINDS NOTHING NEW. TWO COMPONENTS.
| Breadth-first search | Depth-first search | |
|---|---|---|
| Next vertex to explore | earliest discovered (queue) | latest discovered (stack / recursion) |
| Computes | δ(s, v) and a shortest-path tree | a parent tree and a finishing order |
| Time | O(|V| + |E|) | O(|V| + |E|) |
| Extra space | the queue: up to one level | the stack: up to one path |
| Good for | fewest-edge paths, levels, distances | ordering, cycle detection, components, structure |
| Same skeleton | discover, mark seen, scan neighbors | discover, mark seen, scan neighbors |
Swap the queue for a stack and BFS becomes DFS. Everything else is the same visited-set discipline.
Ordering a DAG with DFS finishing times
A directed acyclic graph (DAG) is a directed graph with no directed cycle. A topological order of G lists every vertex so that each edge (u, v) has u before v.
A TASK DAG: A BEFORE B AND C, D NEEDS B AND C, F NEEDS D AND E.
Run Full-DFS on a DAG. The reverse of its finishing order is a topological order.
t = DISCOVERY TIME. FINISHING ORDER F, D, B, E, C, A.
One Full-DFS plus a list reversal: O(|V| + |E|). Linear, like everything today.
F → A IS A BACK EDGE: A IS ON THE STACK. CYCLE A → B → D → F → A.
1 def topological_sort(adj): 2 parent, finished, on_stack = {}, [], set() 3 def visit(u): 4 on_stack.add(u) 5 for v in adj[u]: 6 if v in on_stack: 7 raise ValueError(f'cycle through {v}') 8 if v not in parent: 9 parent[v] = u 10 visit(v) 11 on_stack.remove(u) 12 finished.append(u) 13 for s in adj: # Full-DFS 14 if s not in parent: 15 parent[s] = None 16 visit(s) 17 return list(reversed(finished))
on_stack is the set of ancestors of the current vertex: exactly the vertices whose visit has started but not finished.on_stack is a back edge, and a cycle.Trace all three by hand, in pairs
Side 1: run BFS from S on the graph (levels and parents), then DFS from S (discovery and finishing order), neighbors in alphabetical order. Side 2: topological sort a task DAG by reverse finishing order, then add one edge and find the cycle.
What to keep, and what comes next