Graphs/Graph Representations and Traversal

Lesson 6.12,704 words

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 two explorations you'll use constantly: breadth-first search, which finds shortest paths by number of edges, and depth-first search, whose discovery and finish times reveal a graph's hidden structure.

╌╌╌╌

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. Mastering a handful of graph algorithms gives you a lever on a startlingly wide range of problems; Skiena's blunt advice is that the hardest part is usually recognizing that your problem is a graph problem in disguise.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 small undirected graph on five vertices used throughout the 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, which is exactly 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.

OperationAdjacency listAdjacency matrix
Space
Test edge ?
List neighbors of
Add an edge
Iterate over all edges
Best whengraph is sparsegraph 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 five-vertex graph stored two ways: adjacency lists (left) and the symmetric adjacency matrix (right).

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.

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.

Algorithm 1:Whatever-First-Search(G,s)\textsc{Whatever-First-Search}(G, s) — the generic skeleton
  1. 1
    foreach vertex vVv \in V do
  2. 2
    v.visitedfalsev.visited \gets \text{false}
  3. 3
    v.πnilv.\pi \gets \text{nil}
  4. 4
    s.visitedtrues.visited \gets \text{true}
  5. 5
    put ss into the bag BB
  6. 6
    while BB \neq \emptyset do
  7. 7
    take a vertex uu out of BB
    bag decides who
  8. 8
    foreach vv adjacent to uu do
  9. 9
    if not v.visitedv.visited then
  10. 10
    v.visitedtruev.visited \gets \text{true}
  11. 11
    v.πuv.\pi \gets u
  12. 12
    put vv into the bag BB

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 removalSpecialization
Queue (FIFO)oldest first — explores in rings
Stack (LIFO)newest first — plunges and backtracks
Priority queuecheapest firstDijkstra / 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.

Same graph, same source: the queue (BFS) builds a shallow bushy tree, the stack (DFS) a deep spine.

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 exactly 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.

Algorithm 2:BFS(G,s)\textsc{BFS}(G, s) — shortest distances in hops from ss
  1. 1
    foreach vertex uV{s}u \in V \setminus \set{s} do
  2. 2
    u.colorwhiteu.color \gets \text{white}
  3. 3
    u.du.d \gets \infty
  4. 4
    u.πnilu.\pi \gets \text{nil}
  5. 5
    s.colorgrays.color \gets \text{gray}
    discover source
  6. 6
    s.d0s.d \gets 0
  7. 7
    s.πnils.\pi \gets \text{nil}
  8. 8
    QQ \gets \emptyset
  9. 9
    enqueue(Q,s)(Q, s)
  10. 10
    while QQ \neq \emptyset do
  11. 11
    uu \gets dequeue(Q)(Q)
  12. 12
    foreach vv adjacent to uu do
  13. 13
    if v.color=whitev.color = \text{white} then
    first time reaching v
  14. 14
    v.colorgrayv.color \gets \text{gray}
  15. 15
    v.du.d+1v.d \gets u.d + 1
  16. 16
    v.πuv.\pi \gets u
  17. 17
    enqueue(Q,v)(Q, v)
  18. 18
    u.colorblacku.color \gets \text{black}
  19. 19
    return dd and π\pi

Shortest hops, and why it is correct. Write for the true distance from to , the length of the shortest directed path. The claim BFS delivers is the cleanest possible: for every . Two facts drive the proof:

  • The queue is sorted by depth. At every moment the -values in span at most two consecutive layers , and never decrease as we dequeue. (BFS only ever appends to the back.)
  • always, because is the length of an actual path (trace the pointers), and no path beats the shortest one.

For the reverse inequality, induct on . A vertex at true distance has some neighbor at true distance ; by induction , and since all depth- vertices are dequeued before any depth- vertex is processed, is white when BFS scans 's list and gets . Hence , and following pointers from back to traces an actual shortest path in reverse: the unique -to- path in the BFS tree.

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, the gold standard for graph algorithms.3

A worked run. Take this small digraph and run BFS from . The queue evolves , discovering vertices in the order . The resulting -values sort the vertices into layers by distance from , which let us read off shortest BFS distances directly:

BFS tree from with vertices sorted into distance layers to .

Thick edges are tree edges ; gray 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. The tree path from down to any vertex spells out a shortest route in hops.

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:

Algorithm 3:Connected-Components(G)\textsc{Connected-Components}(G) — label every vertex's component
  1. 1
    c0c \gets 0
  2. 2
    foreach vertex vVv \in V do
  3. 3
    if not v.visitedv.visited then
  4. 4
    cc+1c \gets c + 1
  5. 5
    run BFS(G,v)\textsc{BFS}(G, v), marking each newly visited vertex with cc

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.

Where BFS fans out in rings, depth-first search (DFS) plunges. It is Whatever-First-Search with a stack: pulling the newest discovered vertex means we always descend from where we just were. From a vertex it follows an edge to an unvisited neighbor, then a neighbor of that, going as deep as possible before backtracking to the most recent vertex with an unexplored edge. The LIFO stack is usually left implicit — it is the recursion call stack.

DFS's gift is a pair of timestamps on each vertex : a discovery (or start) time, which we write , stamped when first turns gray, and a finish time , stamped when turns black after all its descendants are done. A single global clock ticks once per stamp, so on vertices every value in is used exactly once. Unlike BFS, the outer driver restarts DFS from any leftover white vertex, so it covers disconnected pieces too, producing a depth-first forest rather than a single tree.

Algorithm 4:DFS(G)\textsc{DFS}(G) — discovery / finish times for every vertex
  1. 1
    foreach vertex uVu \in V do
  2. 2
    u.colorwhiteu.color \gets \text{white}
  3. 3
    u.πnilu.\pi \gets \text{nil}
  4. 4
    time0time \gets 0
  5. 5
    foreach vertex uVu \in V do
  6. 6
    if u.color=whiteu.color = \text{white} then
  7. 7
    call DFS-Visit(G,u)\textsc{DFS-Visit}(G, u)
Algorithm 5:DFS-Visit(G,u)\textsc{DFS-Visit}(G, u) — explore everything reachable from uu
  1. 1
    timetime+1time \gets time + 1
  2. 2
    u.stimeu.s \gets time
    discover u (turns gray)
  3. 3
    u.colorgrayu.color \gets \text{gray}
  4. 4
    foreach vv adjacent to uu do
  5. 5
    if v.color=whitev.color = \text{white} then
  6. 6
    v.πuv.\pi \gets u
  7. 7
    call DFS-Visit(G,v)\textsc{DFS-Visit}(G, v)
  8. 8
    u.colorblacku.color \gets \text{black}
  9. 9
    timetime+1time \gets time + 1
  10. 10
    u.ftimeu.f \gets time

Like BFS, DFS runs in : the initialization and the outer loop cost , and is called exactly once per vertex (only on white vertices, which it immediately grays), scanning each adjacency list once for total.

A worked DFS, and the nesting structure

Run on the digraph below, starting at and breaking ties alphabetically. Each vertex is labeled . Thick edges are tree edges (the depth-first forest); the dashed edges are non-tree edges, classified next.

DFS forest on a digraph with start and finish times and classified non-tree edges.

The discovery and finish times nest like balanced parentheses. Write each vertex's interval . For any two vertices and , exactly one of three things holds, since they can never partially overlap:

  • the intervals are disjoint, and neither vertex is a descendant of the other (they are incomparable), or
  • one interval contains the other, and the contained vertex is a descendant of the container in the DFS forest.

This is the nesting theorem (CLRS calls it the parenthesis theorem), and it makes the recursion visible.4 Drawing each interval as a bar along the time axis turns the forest into a literal stack of nested brackets — a bar sits strictly inside another exactly when the inner vertex is a descendant of the outer:

DFS intervals drawn as bars on the time axis; one bar nesting inside another marks a descendant.

Above, , so is a descendant of is a descendant of . Two companions sharpen it:

  • White-path theorem. becomes a descendant of if and only if, at the moment is discovered (time ), there is a path consisting entirely of still-white vertices. This is the cleanest test for what will end up under .
  • Cycle theorem. Every back edge lies on a cycle, and every cycle contains at least one back edge. Equivalently, a digraph is acyclic iff DFS finds no back edge, the engine behind cycle detection and topological sort.

Classifying edges

As DFS traverses an edge , the color of at that moment reveals what kind of edge it is, a classification that drives the algorithms of the next lessons. In the worked digraph above:

  • Tree edge. is white: we set , so the edge is . We discover through this edge; it joins the depth-first forest (e.g. , ).
  • Back edge. is gray, an ancestor of still on the recursion stack (, since ). A digraph has a back edge iff it has a cycle, the linchpin of cycle detection and topological sort.
  • Forward edge. is black and a descendant of (: a non-tree edge to an already-finished descendant).
  • Cross edge. is black and not a descendant of (, or the edge between separate tree branches).

In an undirected graph the picture simplifies: every edge is either a tree edge or a back edge, since forward and cross edges cannot occur, because exploring an edge from either endpoint reaches the other while it is still gray.

BFS or DFS?

They are one algorithm, , read with two different bags. Both are linear-time skeletons; the order of removal is the only difference, and it dictates the structure each exposes.

BFS (queue)DFS (stack)
Frontier bagqueue, FIFO — oldest firststack, LIFO — newest first
Search treeshallowest paths from nesting / parenthesis structure
Computes (shortest hops)start/finish times
Revealslevel sets, connectivityback edges → cycles, finish order
Typical usesunweighted shortest paths, componentscycle detection, topological sort, strong connectivity

Choose BFS when distance-in-hops matters; choose DFS when you need a graph's recursive structure, which, as the next two lessons show, is exactly what topological sorting and strong connectivity demand. And keep the frame in mind: swap the bag for a priority queue and the very same skeleton becomes Dijkstra and Prim.

Takeaways

  • A graph models things and their connections; the sparse-versus-dense distinction drives every representation choice.
  • Prefer the adjacency list ( space, fast neighbor iteration); use the adjacency matrix () only for dense graphs or constant-time edge tests.
  • is the one skeleton: grow a frontier bag, and let the bag's discipline pick the next vertex. A queue gives BFS, a stack gives DFS, a priority queue gives the weighted algorithms to come.
  • BFS explores in rings from a source and computes , the shortest path in hops, building a breadth-first tree via a FIFO queue.
  • DFS plunges and backtracks, stamping start/finish times that nest like parentheses (nesting + white-path theorems) and classify edges; most importantly, a back edge exists exactly when the graph has a cycle.
  • Both traversals run in , linear in the graph's size, and either one labels connected components for free.

Footnotes

  1. Skiena, §5 — Graph Traversal — the hardest part is recognizing a problem as a graph problem.
  2. CLRS, Ch. 22 — Elementary Graph Algorithms — adjacency list versus adjacency matrix and when each is preferred.
  3. CLRS, Ch. 22 — Elementary Graph Algorithms — BFS computes shortest hop-distances in .
  4. Erickson, Ch. 6 — Graph Search — DFS start/finish times and the parenthesis (nesting) theorem.
Practice

╌╌ END ╌╌