Graph Representations and Traversal
A graph captures relationships — who connects to whom. We fix the vocabulary, weigh the two standard representations (adjacency list versus matrix), then meet the single search skeleton behind everything that follows: Whatever-First-Search, and its breadth-first reading, which finds shortest paths by number of edges in .
╌╌╌╌
Almost every interesting structure (a road map, a social network, the dependencies between tasks, the states of a puzzle) is a set of things and the connections between them. A graph is the mathematical object that captures exactly this and nothing more. A handful of graph algorithms covers a wide range of problems; Skiena's advice is that the hardest part is usually recognizing that a problem is a graph problem.1
What is a graph?
If edges have no direction, so that an edge connects and symmetrically, the graph is undirected. If each edge is an ordered pair pointing from to , the graph is directed (a digraph). We write for the number of vertices and for the number of edges; inside asymptotic notation we abbreviate these to and , writing bounds like .
The definition is fussy for a reason: every clause rules out a pathology. A graph is finite (otherwise we cannot index vertices), edges are unordered pairs (ordered pairs would give a digraph), there are no parallel edges (since is a set, not a multiset), and no self-loops (since for every ). Relaxing any one clause yields a richer object: a multigraph, a digraph, and so on.
A few terms recur constantly:
- Vertices and are adjacent if an edge joins them; that edge is incident on both, which are its endpoints.
- The degree counts the edges touching . In a digraph leaves (its tail) and arrives at (its head), and we split degree into and .
- The handshake lemma falls straight out of counting incidences both ways: in a graph, and in a digraph.
- A walk is an alternating sequence respecting incidence, of length . A walk with
is closed. A path is a walk with no repeated vertices; a cycle is a
closed walk whose vertices are distinct except for the shared endpoint
(length in a graph, in a digraph). Beware: many texts overload
path
to mean walk, but we keep them separate. - An undirected graph is connected if a path joins every pair of vertices; a digraph is strongly connected if a directed path runs both ways between every pair. A connected component is a maximal connected subgraph.
- The distance is the length of the shortest path from to (directed, for digraphs), or if is unreachable from .
- A graph may carry a weight on each edge (a length, cost, or capacity) that later lessons will exploit.
Here is a small undirected graph on five vertices that we will use throughout this lesson:
A graph is bounded in size: every simple graph has at most edges, so . A graph is sparse when is close to and dense when is close to . This single distinction governs which representation, and sometimes which algorithm, to choose.
Two ways to store a graph
We need a concrete data structure before we can compute anything. The two standard choices trade space against the speed of one key query: is there an edge from to ?
Adjacency list. Keep an array indexed by vertex; entry holds a list of 's neighbors. Total space is , one slot per vertex plus one list node per edge (two, in an undirected graph, since each edge appears on both endpoints' lists). Listing a vertex's neighbors is immediate — just what traversals need.
Adjacency matrix. Keep an matrix with when edge exists and otherwise (or the weight, for a weighted graph). Testing a specific edge is , but the matrix always occupies space regardless of how few edges there are, and listing a vertex's neighbors costs because we must scan a whole row.
| Operation | Adjacency list | Adjacency matrix |
|---|---|---|
| Space | ||
| Test edge ? | ||
| List neighbors of | ||
| Add an edge | ||
| Iterate over all edges | ||
| Best when | graph is sparse | graph is dense |
Concretely, here is the five-vertex graph above stored both ways. The list keeps one short neighbor-list per vertex; the matrix spends a full grid of bits, symmetric across the diagonal because the graph is undirected:
The row-scan cost is the decisive one. Every traversal below spends its time
asking give me the neighbors of ,
once per vertex. With lists, the total
work is by the
handshake lemma — that is where the bound comes from. With a matrix,
the same sweep costs no matter how few
edges exist. On a sparse graph with vertices and
edges, the list stores about entries, while the matrix
stores cells; a single BFS does units of work
versus . The break-even point sits around : only when
most possible edges are present does the matrix's edge test and
cache-friendly layout pay for its quadratic footprint.
CLRS, Skiena, and Erickson all reach the same verdict: the adjacency list is the default.2 Real graphs are usually sparse, and the linear-space, fast-to-iterate list is what makes the traversals below possible. Reach for the matrix only when the graph is dense, when you need constant-time edge tests, or when an algorithm is naturally phrased in linear-algebra terms (powers of count walks; spectral methods want the matrix by definition).
One traversal to rule them all
Before specializing, it pays to see that BFS and DFS are the same algorithm. This is made explicit with a deliberately generic skeleton called Whatever-First Search: grow a frontier of discovered-but-unprocessed vertices, repeatedly pull one out, and push each of its undiscovered neighbors in. The only freedom is which vertex you pull next, and that is decided entirely by the data structure holding the frontier.
- 1foreach vertex do
- 2
- 3
- 4
- 5put into the bag
- 6while do
- 7take a vertex out ofbag decides who
- 8foreach adjacent to do
- 9if not then
- 10
- 11
- 12put into the bag
The pointers always carve out a tree (or forest) rooted at , the search tree, because each vertex is discovered exactly once, from exactly one parent. What changes is the shape of that tree, and it is fixed by one choice:
| Bag | Order of removal | Specialization |
|---|---|---|
| Queue (FIFO) | oldest first | — explores in rings |
| Stack (LIFO) | newest first | — plunges and backtracks |
| Priority queue | cheapest first | Dijkstra / Prim (later lessons) |
This is the unifying idea to carry forward: BFS is the queue instantiation, DFS is the stack instantiation, and the weighted shortest-path and minimum-spanning-tree algorithms of later lessons are just Whatever-First-Search with a priority queue. Everything below specializes this one skeleton.
The choice of bag shows up as the shape of the search tree. Run both on the same little graph from (ties broken alphabetically): the queue grows a short, bushy tree that hugs at every depth, while the stack grows one long descending spine, diving as far as it can before backing up.
Breadth-first search
The most basic question we can ask is: starting from a source , which vertices can I reach, and how far away is each? Breadth-first search (BFS) is Whatever-First-Search with a queue. It explores in rings of increasing distance: first itself, then all neighbors of , then everything new one step beyond them, and so on. The first-in-first-out discipline is what enforces this level-by-level order; the oldest-discovered vertex always sits at the shallowest depth still unfinished.
As it runs, BFS computes for each vertex a distance , the fewest edges
(hops
) on any path from to , and a predecessor , the vertex from
which was discovered. The predecessors form the breadth-first tree (or
shortest-path tree) . We refine the
single visited flag of the skeleton into three colors: white vertices are
undiscovered, gray ones are discovered but still in the queue, and black
ones are finished.
- 1foreach vertex do
- 2
- 3
- 4
- 5discover source
- 6
- 7
- 8
- 9enqueue
- 10while do
- 11dequeue
- 12foreach adjacent to do
- 13if thenfirst time reaching v
- 14
- 15
- 16
- 17enqueue
- 18
- 19return and
Write for the true distance from to , the length of the shortest directed path. BFS computes it exactly.
Running time. Initialization touches every vertex once: . Each vertex is enqueued and dequeued exactly once (only white vertices are enqueued, and they are immediately grayed), and when we dequeue we scan its adjacency list once. The scans together examine every edge a constant number of times, for total. Hence BFS runs in , linear in the size of the graph.3
A worked run. Take the digraph with vertices and directed edges
run BFS from , and scan each adjacency list alphabetically. Every row below is one iteration of the while loop: dequeue , scan 's list, enqueue each white neighbor with distance .
| Dequeue | 's list | Newly discovered () | Queue after |
|---|---|---|---|
| — (init) | — | ||
| , , , all | |||
| none ( gray) | |||
| , | |||
| , ( gray) | |||
| , | |||
| — | none | ||
| — | none |
Two things to watch in the table. The -values leaving the queue never decrease (), the sortedness the proof leaned on. And each non-tree edge is examined but discovers nothing: arrives while is gray, while is gray. The resulting -values sort the vertices into layers by distance from , which let us read off shortest BFS distances directly:
Thick edges are tree edges ; dashed edges point at vertices already discovered, so BFS skips them. Reading off : ; ; ; . Vertex has no path from , so , and it never enters the queue — BFS computes distances from the source, and unreachable vertices simply stay white. The tree path from down to any vertex spells out a shortest route in hops.
The same run, viewed as snapshots between layers, shows the queue acting as a moving ring: at any instant it holds the frontier, the gray vertices whose edges have not been scanned yet, and each pass pushes the ring one hop outward:
Vertex stays white in every panel: no ring ever reaches it. The frontier never holds vertices from more than two adjacent layers, and once a layer is fully dequeued the next layer is fully discovered — this is the queue invariant from the correctness proof, drawn.
Reachability and components, for free. The skeleton already solves more than distances. To list the connected components of an undirected graph, loop over all vertices and start a fresh search from each still-unvisited one, tagging every vertex it reaches with the current component number:
- 1
- 2foreach vertex do
- 3if not then
- 4
- 5run , marking each newly visited vertex with
Each search marks exactly one component, and every vertex is visited once, so the whole sweep is still . Because we only used the visited flag, any instantiation works here — swap in DFS and nothing changes. This is the payoff of the unifying view: connectivity is a Whatever-First-Search property, not a BFS one.
BFS reads the search skeleton with a queue and exposes shortest hop-distances. Swap the queue for a stack and the same skeleton plunges instead of fanning out, exposing a graph's recursive structure — the timestamps and edge classification that the rest of this module is built on. This continues in Depth-First Search.
Footnotes
╌╌ END ╌╌