Adjacency matrix vs adjacency list. Space-time tradeoffs.
Deep-dive lesson - accessible entry point but dense material. Use worked examples and spaced repetition.
A graph is only as useful as the way you store it. The same graph can feel “fast” or “slow” depending on whether you represent edges as a dense table (matrix) or as sparse neighbor lists.
Two standard graph representations are:
Matrices give O(1) edge-existence queries but cost Θ(V²) space; lists use Θ(V + E) space and make “iterate neighbors” fast, but checking if a specific edge exists may be slower unless you add extra indexing.
When you learn graphs, it’s natural to focus on the mathematical object: a set of vertices V and edges E. But when you implement algorithms (BFS, DFS, shortest paths, connectivity checks), you must choose a data structure. That choice controls:
Many graph algorithms are dominated by how quickly you can iterate over neighbors. Others frequently ask membership questions (“is there an edge here?”). Different representations optimize different patterns.
We’ll focus on two core representations:
1) Adjacency matrix
2) Adjacency list
We’ll use:
A graph is called dense when E is close to V², and sparse when E is much smaller than V² (for example E ≈ V, or E ≈ V log V).
A quick density intuition:
So V² is the scale of “all possible edges.”
A matrix spends memory on all possible edges. A list spends memory only on existing edges. That single idea drives most of the space-time tradeoffs you’ll see in practice.
An adjacency matrix is conceptually simple: you create a table where rows and columns correspond to vertices. Then:
The main motivation is constant-time edge queries:
Let the vertices be {0, 1, …, V−1}. The adjacency matrix A is a V×V array such that:
A common choice for weighted graphs is:
A matrix allocates V² entries.
This is great if the graph is dense (lots of edges), but wasteful if the graph is sparse.
With an adjacency matrix:
That last bullet is the key weakness: even if v has only 3 neighbors, you still scan V possible neighbors.
Suppose V = 4 with undirected edges:
Then A is:
You can answer “Is there an edge 2—3?” by checking A[2][3] (and A[3][2]).
Matrices are especially good when:
Even if you don’t do advanced math, matrices can be convenient for small graphs because they’re simple to implement and debug.
In many real-world graphs (social networks, web links, road networks), each node connects to a relatively small number of neighbors compared to V. That means the graph is sparse.
If you used a matrix, you would allocate V² space even though only E edges exist.
An adjacency list stores only the edges that exist, so it scales with E.
Adj is an array (indexed by vertex) where each entry is a collection of neighbors.
So you might see:
Adjacency lists store:
Asymptotically:
This is dramatically better than Θ(V²) when E ≪ V².
With an adjacency list:
This is a huge win for traversal algorithms (BFS/DFS) because they naturally iterate neighbors.
But membership queries can be slower:
You can improve this by changing the per-vertex container:
However, these choices have their own tradeoffs (overhead, insertion time, ordering).
Using the same undirected graph:
Adj could be:
Now “iterate neighbors of 2” is just scanning [0, 3], which is Θ(2), not Θ(V).
Adjacency lists are especially good when:
They are the default choice for many algorithmic problems because many classic running times are expressed as Θ(V + E), and adjacency lists make that achievable.
Most decisions reduce to two questions:
1) How often do you need to ask: “Does (i, j) exist?”
2) How often do you need: “List all neighbors of v.”
Adjacency matrix is best for (1).
Adjacency list is best for (2) on sparse graphs.
| Operation / Property | Adjacency matrix A[i][j] | Adjacency list Adj[v] |
|---|---|---|
| Space | Θ(V²) | Θ(V + E) |
| Check edge (i, j) exists | Θ(1) | Θ(deg(i)) (plain list) |
| Add/remove edge | Θ(1) | Θ(1) amortized to append (remove can be Θ(deg(i))) |
| Iterate neighbors of v | Θ(V) | Θ(deg(v)) |
| Best for | Dense graphs, many membership queries, matrix math | Sparse graphs, traversals, scalable storage |
| Undirected storage | symmetric A | store each edge twice |
Compare E to V².
A concrete mental check:
BFS/DFS often have a natural bound of Θ(V + E) when using adjacency lists:
With an adjacency matrix, scanning neighbors costs Θ(V) per vertex, so traversal becomes Θ(V²), regardless of how few edges exist.
This is one of the main reasons adjacency lists are the standard representation for graph traversal lessons.
Spectral methods use linear algebra objects like:
Eigenvalues/eigenvectors are defined for matrices, so matrix representations (or at least matrix-like operators) are the natural language.
Even if the graph is sparse, spectral computations often rely on sparse-matrix data structures—but the core conceptual object remains A.
Be careful with weighted graphs:
In adjacency lists, the absence of an edge is natural: if (j, w) is not in Adj[i], the edge is absent.
If you later find you need both benefits, you can maintain both structures—but that increases code complexity and memory.
Let V = {0,1,2,3}. Directed edges: 0→1, 0→2, 2→1, 3→0. Unweighted. Construct the adjacency matrix A and adjacency list Adj.
Step 1: Initialize A as a 4×4 matrix of zeros.
A =
[ [0,0,0,0],
[0,0,0,0],
[0,0,0,0],
[0,0,0,0] ]
Step 2: Fill in A[i][j] = 1 for each directed edge i→j.
Edges:
Step 3: Write the final matrix explicitly by rows.
Row 0 has edges to 1 and 2: [0,1,1,0]
Row 1 has no outgoing edges: [0,0,0,0]
Row 2 has edge to 1: [0,1,0,0]
Row 3 has edge to 0: [1,0,0,0]
So
A =
[ [0,1,1,0],
[0,0,0,0],
[0,1,0,0],
[1,0,0,0] ]
Step 4: Build Adj as an array of 4 lists, initially empty.
Adj[0]=[ ]
Adj[1]=[ ]
Adj[2]=[ ]
Adj[3]=[ ]
Step 5: Append each edge i→j to Adj[i].
Final:
Adj[0]=[1,2]
Adj[1]=[ ]
Adj[2]=[1]
Adj[3]=[0]
Insight: A and Adj store the same information, but support different fast operations: A gives O(1) edge checks like “is 2→1 present?”, while Adj makes it cheap to enumerate outgoing neighbors like “who can 0 reach in one step?”
Suppose V = 1000 and E = 3000 (directed). Estimate the number of stored entries for an adjacency matrix vs an adjacency list, and compare the cost of iterating neighbors across all vertices (as in BFS/DFS).
Step 1: Adjacency matrix storage.
A has V² entries.
V² = 1000² = 1,000,000 entries.
So storage is Θ(1,000,000) cells (times the size per cell).
Step 2: Adjacency list storage.
Adj stores:
Total list items ≈ V + E = 1000 + 3000 = 4000 (ignoring overhead).
So storage is Θ(4000) items.
Step 3: Neighbor iteration cost for a traversal.
A typical traversal needs to scan neighbors of each vertex.
With a matrix:
With a list:
So total cost is Θ(V + E) = Θ(1000 + 3000) = Θ(4000).
Step 4: Interpret the result.
Matrix neighbor scans touch 1,000,000 potential edges.
List neighbor scans touch only the 3,000 real edges (plus vertex overhead).
Insight: On sparse graphs, adjacency lists turn many graph algorithms into “work proportional to what exists” (V + E), while adjacency matrices force you to repeatedly scan nonexistent edges, pushing you toward V² time.
Vertices {0,1,2}. Directed weighted edges: 0→1 with weight 5, 0→2 with weight 0, 2→1 with weight −2. Build both representations and highlight the ‘no edge’ issue.
Step 1: Notice weight 0 is a valid edge weight (0→2 has weight 0).
Therefore, using 0 to mean “no edge” in a matrix would be ambiguous.
Step 2: Choose a sentinel for ‘no edge’ in the matrix.
A common choice is ∞.
Initialize A as a 3×3 matrix with ∞ everywhere, and set diagonal to 0:
A =
[ [0, ∞, ∞],
[∞, 0, ∞],
[∞, ∞, 0] ]
Step 3: Fill in edge weights.
So
A =
[ [0, 5, 0],
[∞, 0, ∞],
[∞, −2, 0] ]
Step 4: Build adjacency list with (neighbor, weight) pairs.
Adj[0] = [(1,5), (2,0)]
Adj[1] = [ ]
Adj[2] = [(1,−2)]
Step 5: Compare ‘no edge’ meaning.
In the matrix, A[1][0] = ∞ explicitly stores “no edge.”
In the list, the absence of 0 from Adj[1] implicitly means “no edge.”
Insight: Weighted matrices require a careful choice of sentinel (∞, null) to represent missing edges, especially when 0 or negative weights are allowed. Adjacency lists avoid this ambiguity by storing only real edges.
Adjacency matrix A is a V×V table where A[i][j] records whether/what weight edge i → j exists.
Adjacency list Adj stores, for each vertex v, exactly the neighbors reachable from v (and optionally weights).
Space: matrix is Θ(V²); list is Θ(V + E) (or Θ(V + 2E) for undirected storage, which is still Θ(V + E)).
Edge-existence checks are Θ(1) with matrices but typically Θ(deg(i)) with plain adjacency lists.
Neighbor iteration is Θ(V) per vertex in matrices but Θ(deg(v)) per vertex in lists—crucial for BFS/DFS.
Sparse graphs strongly favor adjacency lists; dense graphs (or heavy edge-query workloads) can favor matrices.
Weighted graphs in matrix form need a well-defined ‘no edge’ sentinel (often ∞) to avoid ambiguity.
Using 0 in a weighted adjacency matrix to mean “no edge” when weight 0 is a valid value; prefer ∞/null or a separate boolean matrix.
Forgetting that undirected graphs require symmetric storage: A[i][j] = A[j][i] and u in Adj[v] implies v in Adj[u].
Assuming adjacency lists always give O(1) edge-existence checks; with a plain list it’s Θ(deg(i)) unless you add a set/hash structure.
Implementing BFS/DFS over an adjacency matrix and expecting Θ(V + E); neighbor scans make it Θ(V²) in general.
You have an undirected unweighted graph with V = 6 and edges: 0—1, 0—4, 1—2, 2—3, 3—4. Write (a) its adjacency list Adj and (b) one row of its adjacency matrix A (specifically row 0).
Hint: Undirected means each edge appears in both endpoints’ neighbor lists, and A is symmetric. Row 0 marks neighbors of 0 with 1s.
Adj:
Adj[0] = [1,4]
Adj[1] = [0,2]
Adj[2] = [1,3]
Adj[3] = [2,4]
Adj[4] = [0,3]
Adj[5] = [ ]
Row 0 of A (columns 0..5):
A[0] = [0,1,0,0,1,0]
For a directed graph, V = 2000 and E = 10,000. Compare Θ(V²) and Θ(V + E) numerically, and explain which representation is likely better for BFS and why.
Hint: Compute V² and V + E. BFS mainly needs to iterate neighbors of visited nodes.
V² = 2000² = 4,000,000.
V + E = 2000 + 10,000 = 12,000.
For BFS, adjacency lists are likely better because BFS iterates over outgoing neighbors; with lists this costs Θ(V + E) ≈ 12,000 neighbor entries plus overhead, while a matrix-based BFS would scan Θ(V) = 2000 potential neighbors per vertex, pushing toward Θ(V²) ≈ 4,000,000 checks.
You need to support many queries of the form “is there an edge (i, j)?” on a sparse graph. Propose one way to keep the adjacency-list space advantage but speed up edge existence checks.
Hint: Change the per-vertex container type or add an auxiliary index structure.
One approach is to store Adj[v] as a hash set (or maintain an additional hash set alongside a list). Then checking whether j ∈ Adj[i] can be expected Θ(1), while total space remains proportional to Θ(V + E) (with higher constant factors). Another approach is to keep Adj[v] sorted and use binary search for Θ(log deg(v)) membership queries.
Next nodes: