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.
Motivation
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 choice of frontier answers different questions:
- 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.
Definitions
Definition
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.
A path, in the course terminology, is a vertex sequence in which every consecutive pair is joined by an edge; it may repeat vertices. A simple path does not repeat vertices. A path's unweighted length is its number of edges, not its number of vertices. A cycle begins and ends at the same vertex; a simple cycle repeats no other vertex. A graph with no cycle is acyclic.
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
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
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.
Theorem/proposition
Theorem
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
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 a search from one source, the sharper bound uses only the reached vertices and scanned outgoing edges. The conventional whole-graph bound is stated because the outer-loop versions visit every component.
Proof sketch or proof idea
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).
Worked examples
Use the course graph on vertices A,B,C,D,E,F. It is directed, and we fix the
following adjacency-list order:
A: B, D
B: C, D
C: F, D, E
D: E
E: —
F: —
Worked example
Convert the course 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
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
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
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 but costs quadratic space and row scans; 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.