A graph separates two questions that are easy to mix up: what relations exist, and how those relations are stored. Depth-first search (DFS) and breadth-first search (BFS) operate on the same abstract graph, but their frontiers impose different exploration orders. A rigorous trace must therefore state the graph representation, the order in which neighbours are inspected, and exactly when a vertex becomes discovered.
Choosing the next vertex
Graphs model roads, communication links, prerequisites, and program states. Unlike an array, a graph does not come with one natural “next” element. From a vertex there may be no outgoing edge, one outgoing edge, or many. A traversal needs a disciplined frontier and a record of what has already been discovered.
The stack and queue contracts give the frontier two different orders:
- DFS follows one unfinished route deeply and is naturally expressed by recursion or a stack.
- BFS processes the oldest discovered vertex first and therefore expands in distance layers through a queue.
Neither algorithm requires edge weights merely to visit vertices. When every edge has equal cost, however, BFS provides more: it computes the minimum number of edges from the source to every reachable vertex.
Graphs, paths, and reachability
Definition 5.1.1
(Graph and edge types)
A graph is written G = (V,E), where V is a set of vertices and E is a set
of edges. In a directed graph an edge (u,v) goes from u to v; it does not
automatically permit travel from v to u. In an undirected graph an edge
joins its endpoints in both directions. A weighted graph attaches a numerical
weight to each edge; an unweighted graph does not distinguish edges by cost.
Here a path is a vertex sequence whose consecutive vertices are joined by an edge; this broad usage is also called a walk, and vertices or edges may repeat. A simple path has no repeated vertex. Its unweighted length counts traversed edges, not vertices.
A closed walk starts and ends at the same vertex. A trail repeats no edge, and a closed trail is a closed walk with no repeated edge. Here cycle means simple cycle: a closed trail with at least one edge and no repeated vertex except its first and last. A graph with no cycle is acyclic.
Unless stated otherwise, the undirected examples use simple graphs, with no loops or parallel edges. Their cycles have at least three edges. Traversing A -> B -> A along one undirected edge uses that same edge twice, so it is a closed walk but not a cycle. In a directed graph, a cycle follows the arc directions: distinct arcs A -> B and B -> A can form a two-edge cycle. A self-loop is a one-edge cycle only in a model that permits loops. In an undirected multigraph, two distinct parallel edges can form a two-edge cycle, but distinguishing them requires edge identities. The traversal sketches below record vertices; they are not edge-identity-aware multigraph cycle tests.
Why a closed trail contains a cycle. Among the positive-length closed segments of a nonempty closed trail, choose one with the fewest edges. Any repetition of a vertex other than its identified endpoints would delimit a shorter positive-length closed segment. Thus the selected segment has no such repetition and is a cycle. Conversely every cycle is a nonempty closed trail. Therefore absence of cycles is equivalent to absence of nonempty closed trails; it is not equivalent to absence of closed walks.
Vertex v is reachable from u if a directed path from u to v exists.
Reachability need not be symmetric in a directed graph. In an undirected graph,
two vertices are connected when a path joins them, and the graph is connected
when every pair is connected. A maximal connected part is a connected
component. For a directed vertex, out-degree counts leaving edges and in-degree
counts entering edges.
Choosing a representation
Let the vertices be indexed 0,1,...,|V|-1.
Definition 5.1.2
(Adjacency matrix)
An adjacency matrix is a |V| × |V| array M in which M[u][v] records
whether the edge (u,v) exists. In a weighted graph the entry can store the
weight, together with an unambiguous convention for “no edge.” An undirected
graph produces a symmetric matrix.
Definition 5.1.3
(Adjacency list)
An adjacency list stores, for every vertex u, a list Adj[u] of the vertices
reached by outgoing edges from u. A weighted list stores a neighbour-weight
pair for each outgoing edge.
The representation changes the cost of obtaining neighbours:
| Operation | Adjacency matrix | Adjacency list |
|---|---|---|
| Store the graph | `Theta( | V |
Test whether (u,v) exists | Theta(1) | O(out-degree(u)) |
Enumerate all outgoing neighbours of u | `Theta( | V |
| Traverse the whole graph | `Theta( | V |
An adjacency matrix can be appropriate for a dense graph or repeated direct edge queries. An adjacency list avoids scanning absent edges and is therefore the natural representation for the linear-time traversals analysed below. In an undirected adjacency list, each edge appears in two lists; this changes a factor of two, not the asymptotic bound.
Depth-first search
DFS discovers a vertex, chooses its first undiscovered neighbour, and suspends the current vertex while it explores that neighbour. Recursive calls remember the suspended work. When a vertex has no undiscovered neighbour left, the call finishes and control backtracks.
DFS-VISIT(u):
discovered[u] = true
process u
for each v in Adj[u], in the stated order:
if not discovered[v]:
parent[v] = u
DFS-VISIT(v)
DFS-FOREST(G):
set every vertex to undiscovered and every parent to null
for each vertex u in the stated vertex order:
if not discovered[u]:
DFS-VISIT(u)
Calling DFS-VISIT(s) explores exactly the vertices reachable from s.
DFS-FOREST adds the outer loop needed to cover a disconnected graph. Each
root produced by that loop begins another DFS tree. An explicit stack can
replace recursion, but it must preserve the same neighbour-order convention if
the same trace is desired: pushing several neighbours onto a last-in-first-out
stack can reverse their processing order.
The parent edges form a DFS tree (or a forest under the outer loop). They record how each vertex was first discovered; non-tree edges remain part of the graph even though they are not parent edges.
Breadth-first search and its frontier invariant
BFS uses a first-in-first-out queue. The queue contains discovered vertices whose outgoing adjacency lists have not yet been completely processed. The safe discovery rule is crucial:
Mark a vertex discovered when it is enqueued, not later when it is dequeued.
BFS(G, s):
for each vertex v:
discovered[v] = false
distance[v] = infinity
parent[v] = null
discovered[s] = true
distance[s] = 0
enqueue(Q, s)
while Q is not empty:
u = dequeue(Q)
process u
for each v in Adj[u], in the stated order:
if not discovered[v]:
discovered[v] = true
distance[v] = distance[u] + 1
parent[v] = u
enqueue(Q, v)
The assignment to discovered[v] occurs before enqueue(Q,v). Thus a second
frontier vertex that also has an edge to v sees that v is already claimed
and cannot enqueue it again. “Discovered” and “fully processed” are different
states: a queued vertex is discovered even though it has not yet been dequeued.
As with DFS, one call explores only vertices reachable from the source. To traverse every component of an undirected graph, run BFS again from an undiscovered vertex until none remain.
Shortest paths and traversal cost
Theorem 5.1.4
(BFS shortest-path property for unweighted graphs)
When BFS starts at s, every reachable vertex v is first discovered with
distance[v] equal to the minimum number of edges on any path from s to v.
Following parent pointers from v back to s reconstructs such a shortest
path.
This statement is about equal-cost edges. If edges have unequal weights, a path with fewer edges can have greater total weight. Ordinary BFS then still minimizes edge count, but it does not necessarily minimize weighted cost.
Theorem 5.1.5
(Adjacency-list traversal bound)
DFS and BFS run in O(|V|+|E|) time on an adjacency-list graph when the search
covers the whole graph. Their visited, parent, and frontier storage is O(|V|)
in addition to the graph representation.
For one source, let V_R be the reached vertices and E_R their outgoing
edges. The traversal itself takes O(|V_R|+|E_R|). The displayed BFS also
initializes arrays for every vertex, so its total time is O(|V|+|E_R|).
A bound using only the reached part must account for initialization separately.
Why layers and edge counts give the bounds
For the BFS shortest-path property, use a layer invariant. Initially only s
has distance 0. Whenever BFS discovers v while processing u, it assigns
distance[v] = distance[u] + 1. Because the queue is first-in-first-out, every
vertex at distance k is processed before any newly discovered vertex at
distance k+1. Therefore a hypothetical shorter path to v would have had to
reach v from an earlier layer and would have discovered it sooner. The first
assigned distance is consequently minimal. Each parent pointer decreases the
distance by exactly one, so the reconstructed chain has that minimal length.
For the running time, initialization and the outer loop cost O(|V|). A
vertex becomes discovered once, so it enters the DFS recursion/stack or BFS
queue once. Scanning all adjacency lists examines each directed edge once; an
undirected edge is examined twice. Hence the total is O(|V|+|E|). With a
matrix, processing each reached vertex scans a full row of |V| entries, so a
whole-graph traversal is instead Theta(|V|^2).
Two traversals of the same graph
Consider the directed graph on vertices A,B,C,D,E,F, with the
following adjacency-list order:
A: B, D
B: C, D
C: F, D, E
D: E
E: —
F: —
Worked example 5.1.6
(Convert the graph between representations)
With row and column order A,B,C,D,E,F, the same graph has adjacency matrix
A B C D E F
A 0 1 0 1 0 0
B 0 0 1 1 0 0
C 0 0 0 1 1 1
D 0 0 0 0 1 0
E 0 0 0 0 0 0
F 0 0 0 0 0 0
The matrix is not symmetric: for example A -> B exists but B -> A does
not. The list makes the eight outgoing edges explicit without storing the
twenty-eight absent ordered pairs.
Worked example 5.1.7
(Trace recursive DFS from A)
Using the fixed list order, the discoveries are
A, B, C, F, D, E. The active recursive stack changes as follows:
| Event | Active stack after the event |
|---|---|
discover A | [A] |
discover B | [A,B] |
discover C | [A,B,C] |
discover F | [A,B,C,F] |
finish F | [A,B,C] |
discover D | [A,B,C,D] |
discover E | [A,B,C,D,E] |
| finish and backtrack | eventually [] |
When control returns to C, its neighbour E is already discovered through
D, so no second recursive call is made. A different adjacency-list order can
produce a different valid DFS order without changing which vertices are
reachable.
Worked example 5.1.8
(Trace BFS layers, parents, and distances from A)
The queue below is shown after the current vertex has been processed.
| Dequeued | Newly discovered | Queue afterward | New distance/parent |
|---|---|---|---|
A | B,D | [B,D] | d(B)=d(D)=1, parents A |
B | C | [D,C] | d(C)=2, parent B |
D | E | [C,E] | d(E)=2, parent D |
C | F | [E,F] | d(F)=3, parent C |
E | none | [F] | no change |
F | none | [] | no change |
Thus the dequeue order is A,B,D,C,E,F. Parent pointers reconstruct, for
example, the shortest path A -> B -> C -> F, containing three edges. Although
C also has an edge to E, the route A -> B -> C -> E has three edges. BFS
has already discovered E through the shorter two-edge route A -> D -> E.
Worked example 5.1.9
(Why marking only on dequeue creates duplicates)
Suppose A is marked only when removed from the queue. Processing A enqueues
B and D, but leaves both unmarked. The queue is [B,D]. After B is
removed, B examines C and D. Since the earlier copy of D is still
unmarked, B enqueues D again, producing [D,C,D].
The graph has not changed; the implementation has lost the one-enqueue
invariant. It may waste work and can overwrite a parent or distance unless more
checks are added. Marking D at the moment A enqueues it makes B skip the
duplicate. This is why enqueue-time marking is part of BFS correctness, not a
minor coding preference.
Common mistakes
Common mistake
(Treating a traversal order as unique)
DFS and BFS respect their stack/queue disciplines, but ties are resolved by the stated adjacency order. Change that order and the exact trace may change.
Common mistake
(Confusing discovery with completion)
In BFS, a vertex becomes discovered when enqueued and becomes fully processed after it is dequeued and its adjacency list is scanned. Delaying discovery until dequeue permits duplicate queue entries.
- A source search does not automatically visit unreachable vertices; an outer loop is required for a full graph traversal.
- A directed matrix need not be symmetric, and directed reachability need not work in both directions.
- BFS guarantees minimum edge count, not minimum arbitrary weighted cost.
- Quoting
O(|V|+|E|)while actually scanning every row of an adjacency matrix mixes two different representations. - A DFS parent tree or BFS parent tree contains discovery edges, not every edge of the original graph.
Summary
- A graph may be directed or undirected and weighted or unweighted.
- Paths, cycles, reachability, and connectivity describe the abstract graph, independently of its storage representation.
- A matrix supports constant-time edge tests and uses quadratic space. One row
scan is linear in
|V|; scanning every row is quadratic. A list stores and scans existing neighbours. - DFS uses recursion or a stack to go deep before backtracking.
- BFS uses a queue to expand reachable vertices in nondecreasing edge-distance layers.
- Robust BFS marks a vertex at enqueue time, guaranteeing at most one queue entry per vertex.
- BFS parent and distance arrays describe shortest paths in an unweighted graph.
- Adjacency-list DFS and BFS take
O(|V|+|E|)for the whole graph.
Exercises
- For the
A–Fgraph, write the outgoing neighbours ofCand the row of the adjacency matrix belonging toC. Explain why the list cost depends on out-degree while the matrix row scan does not. - Run recursive DFS from
D. Then run BFS fromD. Give the discovery order for each search using the fixed list order, and state which vertices are not reachable. - In the original BFS from
A, explain whyEreceives distance2even thoughCalso has an edge toE. Reconstruct the stored parent path. - Give the earliest queue state that contains a duplicate when visited marks are delayed until dequeue. Identify the two edges responsible.
- A graph has
10,000vertices and20,000directed edges. Compare the number of adjacency positions conceptually scanned by a whole-graph matrix traversal with the list-based asymptotic work.
Solutions
Solution · Guided solutions
-
Adj[C] = [F,D,E], while rowCis0 0 0 1 1 1in column orderA,B,C,D,E,F. The list scans three stored neighbours; the matrix must inspect all six columns, including absent edges. -
From
D, both DFS and BFS discoverD,E, in that order.Ehas no outgoing edge. VerticesA,B,C,Fare unreachable fromD; incoming edges toDdo not permit travel backwards in this directed graph. -
Dis dequeued beforeC, so it discoversEfirst and assignsdistance[E]=distance[D]+1=2withparent[E]=D. WhenCis processed,Eis already discovered. The stored path isA -> D -> E. -
After processing
Aunder delayed marking the queue is[B,D]. ProcessingBfollowsB -> Dwhile the copy inserted throughA -> Dis still unmarked, producing[D,C,D]. -
A matrix traversal scans about
|V|^2 = 100,000,000positions. An adjacency list traversal performsO(|V|+|E|) = O(30,000)vertex-and-edge work, up to constant factors. The contrast comes from scanning only stored edges.