Overview
Graphs are commonly stored as either adjacency lists or an adjacency matrix. Lists store, for each vertex u, the neighbors Adj[u]. A matrix stores a V x V table where entry (i,j) indicates the weight/existence of edge i->j. The choice drives space, iteration cost, and edge lookup cost, and it shapes how algorithms like Dijkstra’s Algorithm or Graph Traversals (BFS & DFS) behave in practice.
Motivation
No single representation dominates. Use lists to keep memory linear in the number of edges and to iterate neighbors quickly on sparse graphs. Use a matrix when the graph is dense, when constant-time edge existence queries are frequent, or when bitset operations can accelerate dense computations (e.g., transitive closure).
Definition and Formalism
Let n = |V| and m = |E| (directed edges count separately).
-
Adjacency list (AL): Data shape:
Adj[u]is a container of neighbors (and, for weighted graphs,(v,w)pairs). Space:Theta(n + m)(plus container overhead). Typical ops: iterate neighbors ofuinTheta(deg(u)). -
Adjacency matrix (AM): Data shape:
n x ntableMwithM[u][v] = weight(or1/true) if edge exists, else0/false/infinity. Space:Theta(n^2)regardless ofm. Typical ops:hasEdge(u,v)andweight(u,v)inTheta(1).
Example or Illustration
Consider the directed graph with edges: 0->1, 0->3, 1->2, 2->3, 3->0. Weights omitted (unweighted) for brevity.
Adjacency list
Adj[0]: {1, 3}
Adj[1]: {2}
Adj[2]: {3}
Adj[3]: {0}
Adj[4]: {}
Adjacency matrix
0 1 2 3 4
0: 0 1 0 1 0
1: 0 0 1 0 0
2: 0 0 0 1 0
3: 1 0 0 0 0
4: 0 0 0 0 0
Tip
For weighted graphs, store weights in
Adj[u]as pairs(v,w)or inM[u][v]as the numeric weight with a sentinel (e.g.,+infinityor0) for “no edge”. Be consistent across the codebase.
Properties and Relationships
-
Sparsity vs density. If
mis much smaller thann^2(e.g.,m = O(n)orO(n log n)), lists typically win on both space and neighbor iteration time. Whenmis approximatelyn^2, the matrix costTheta(n^2)is proportional to actual edges; constant-time lookups pay off. -
Iteration pattern. Many algorithms need “for each neighbor
vofu…” (BFS/DFS, Dijkstra). Lists match this pattern directly; matrices require scanning a full row (costTheta(n)regardless ofdeg(u)). -
Edge tests. When code frequently asks “is
(u,v)an edge?” without iterating neighbors, matrices provideTheta(1)answers; lists require searchingAdj[u](Theta(deg(u))). -
Transitive closure / algebraic methods. Matrix-based algorithms (Warshall, fast boolean matrix multiplies) benefit from AM and bit-packing.
Implementation or Practical Context
Operation Costs (typical)
| Operation | Adjacency List | Adjacency Matrix |
|---|---|---|
| Space | Theta(n + m) | Theta(n^2) |
Iterate neighbors of u | Theta(deg(u)) | Theta(n) |
Check/Fetch weight of (u,v) | Theta(deg(u)) (or log deg(u) with a set/map) | Theta(1) |
Insert edge (u,v) | Amortized Theta(1) (push) | Theta(1) |
Delete edge (u,v) | Theta(deg(u)) unless indexed | Theta(1) |
| Add vertex | Theta(1) (push empty list) | Theta(n) to expand each row/col |
| Remove vertex | Theta(deg(u) + in-edges) | Theta(n) to clear row/col |
Directed vs Undirected
-
Adjacency list: For undirected graphs, store both directions (
u->vandv->u). Degrees:deg(u)counts neighbors; total edges stored =2m. -
Adjacency matrix: Mirror entries:
M[u][v] = M[v][u]for undirected graphs.
Weighted Graphs
-
Lists: Use structs
{to, weight}; optional secondary index if frequent(u,v)lookups are needed (e.g., sort or hash neighbors). -
Matrix: Natural place to keep weights; initialize with
infinity(or0for boolean reachability).
Cache/Memory Considerations
-
AL:
array<vector<int>>(or similar) yields contiguous blocks per vertex; iteration is cache-friendly. Hash-based neighbor sets speed edge checks but hurt iteration latency. -
AM: Dense
n x nblocks traverse well in row-major order; for largen, tile/block loops to keep working sets in cache.
Algorithmic Fit
-
BFS/DFS: Prefer AL for sparse graphs: time
Theta(n + m)vsTheta(n^2)for AM (since AM scans each row). -
Dijkstra: AL with a priority queue is
Theta((n + m) log n); AM version isTheta(n^2)and is competitive only when the graph is dense ornis small. -
Floyd–Warshall / Warshall: Naturally matrix-based; initializing
dist/reachcomes “for free” with AM. See Floyd–Warshall Algorithm. -
Edge-existence-heavy workloads: AM shines (e.g., constraint checking, dense DP transitions).
Common Misunderstandings
Warning
Using a matrix for a very sparse graph. The
Theta(n^2)blow-up wastes memory and makes neighbor scansTheta(n)instead ofTheta(deg(u)). Prefer AL unless density orhasEdgeneeds dominate.
Warning
Mismatched degree logic. In undirected graphs stored as AL, each undirected edge appears twice. Be careful when computing totals like
Sum deg(u) = 2m.
Warning
Inconsistent sentinels. With AM for weighted graphs, pick a single “no edge” sentinel (e.g.,
INF) and check it before arithmetic to avoid overflow.
Warning
Unindexed deletions. Removing
(u,v)fromAdj[u]can beTheta(deg(u)). If deletions are common, maintain positions (e.g., swap-remove) or an auxiliary hash for neighbors.
Broader Implications
Representation choices ripple into:
-
Asymptotic bounds (e.g.,
Theta(n+m)vsTheta(n^2)traversals). -
Engineering constraints (RAM usage for large
n; locality; ease of dynamic updates). -
Parallelization: AM supports SIMD/bitset tricks and straightforward blocking; AL provides natural task parallelism over vertices/edges but requires careful scheduling to avoid contention.
Summary
-
Use adjacency lists when the graph is sparse, you often iterate neighbors, or memory is tight.
-
Use an adjacency matrix when the graph is dense, when edge checks dominate, or when matrix-style algorithms/bitsets are a win.
-
Match algorithms to representation: BFS/DFS/Dijkstra typically prefer AL; Floyd–Warshall and transitive closure like AM. Choosing the right form improves both big-O and real-world performance.