Graph Representation
Graph representation turns vertices and edges into data a program can store and query. An adjacency list keeps one neighbor row per vertex and uses O(V + E) space. An adjacency matrix reserves a V by V grid, spends O(V²) space, and answers has_edge(u, v) with one indexed lookup.
Think of the list as an address book: open vertex u and read only the neighbors it actually has. The matrix is a seating chart with a box for every possible pair, including the pairs that never meet. The empty boxes cost space; the fixed coordinates buy speed.
Graph representation trades space for lookup speed
The lesson uses a simple undirected graph. "Simple" means no parallel edges and no self-loops. "Undirected" means edge u-v has no preferred direction, so both representations must store it symmetrically.
In an adjacency list, edge u-v performs two appends: put v in adj[u], then put u in adj[v]. Each undirected edge therefore contributes two neighbor entries. Across the whole graph, the rows hold exactly 2E entries, plus one row head for each of the V vertices.
In an adjacency matrix, the same edge sets two cells: matrix[u][v] = 1 and matrix[v][u] = 1. The two ones mirror each other across the main diagonal. A zero means no edge. Because this lesson excludes self-loops, the diagonal stays zero.
That symmetry is the invariant. If the list stores both directions but the matrix writes only one cell, the structures describe different graphs. If the list appends only one direction, it has quietly become a directed graph while the picture still claims otherwise.
The two structures make different operations cheap. A list can report degree(u) from the length of row adj[u]. To test whether u-v exists, however, it scans that row until it finds v or reaches the end. A matrix does the reverse trade: degree requires summing an entire row, but edge membership is the single cell matrix[u][v].
Graph representation step by step
The default graph has vertices 0 through 5 and five edges, processed in this order:
0-1, 0-2, 1-3, 2-3, 3-4
Vertex 5 is isolated. That is deliberate; a graph representation should not need special pleading to store a vertex with degree zero.
- Allocate six empty list rows and a 6 by 6 matrix of zeros.
- Encode edge
0-1. Append1toadj[0], append0toadj[1], then set matrix cells[0][1]and[1][0]to1. - Encode edge
0-2the same way. Rowsadj[0]andadj[2]gain mirror entries, and cells[0][2]and[2][0]become1. - Repeat the three writes for
1-3,2-3, and3-4. No write ever touches vertex5. - The finished adjacency list is
0:[1,2],1:[0,3],2:[0,3],3:[1,2,4],4:[3],5:[]. It contains ten neighbor entries: exactly2E = 10. - The matrix contains 36 cells because
V² = 6² = 36. Its row for vertex3is[0,1,1,0,1,0], matching neighbors1,2, and4from the list. - Compute
degree(3). The list readslen(adj[3]) = 3. The matrix sums row3, also obtaining3, but it inspects all six cells to do so. - Ask
has_edge(3, 5). The list compares1with5, then2, then4; the row ends after three probes, so the answer isFalse. - The matrix reads
matrix[3][5]. That one cell is0, so it returns the sameFalseanswer in one lookup.
On the canvas, the source edge stays visible while its list entries and symmetric matrix cells light up. During the query, the list cursor walks neighbor by neighbor; the matrix guides cross at one cell. The motion is not decoration. It is the cost model made visible.
Time and space complexity
Let V be the number of vertices, E the number of undirected edges, and degree(u) the number of neighbors of vertex u.
Let the workload choose. If your program asks millions of arbitrary edge-membership questions, a matrix may earn its space. If it mostly walks the neighbors that exist, an adjacency list avoids scanning a desert of zeros.
| Operation | Adjacency list | Adjacency matrix |
|---|---|---|
| Space | O(V + E) | O(V²) |
| Add undirected edge | O(1) amortized | O(1) |
has_edge(u, v) | O(degree(u)) | O(1) |
degree(u) | O(1) from row length | O(V) from a row sum |
The list wins the usual sparse-graph argument because E is far smaller than V². The default needs six row heads and ten neighbor entries, while the matrix reserves 36 cells.
Dense K5 is the useful correction. A complete five-vertex graph has ten edges, so the list's logical term count is V + 2E = 5 + 20 = 25, exactly the matrix's V² = 25 cells. Those counts are not byte measurements; real arrays, pointers, and row objects have different overhead. They do show why the list's asymptotic space advantage disappears when E = Θ(V²).
Edge cases
- Singleton graph: one vertex, no edges. Its list row is empty, its 1 by 1 matrix contains zero, and
degree(0) = 0. - Isolated vertex: vertex
5in the default has a real row and matrix row even though both contain no neighbors. - Absent edge:
has_edge(3, 5)returningFalseis normal data, not an error condition. - Dense graph: Dense K5 removes the list's sparse-space advantage and makes a late list hit scan four neighbors.
- Self-loops and parallel edges: this lesson excludes both. Supporting them requires an explicit convention about diagonal cells and duplicate list entries.
Common mistakes
- Writing only one list direction. For an undirected edge
u-v, bothadj[u]andadj[v]must change. - Writing only one matrix cell. An undirected adjacency matrix is symmetric, so
[u][v]and[v][u]must agree. - Claiming matrix space is O(E). The grid reserves
V²cells even when almost every cell is zero. - Scanning every vertex for a list query.
has_edge(u, v)scans onlyadj[u], so its bound is O(degree(u)), not automatically O(V + E). - Treating logical entries as bytes.
V + 2EandV²explain growth rates. They do not claim that one list entry occupies the same memory as one matrix cell.
Keep exploring
A few lessons that share the same technique or lead into the next idea.
Found a mistake or something unclear? Leave feedback.