Graph Representations: Adjacency Lists and Matrices

Imagine you are building the routing layer of a social network. It has a billion users and a hundred billion friendship edges. Every time someone loads their feed, the system must ask: who are this person's neighbors? Answering that question a billion times a day forces a decision. It must be made before a single line of traversal code is written. That decision is how the graph itself is stored in memory. This choice is often treated as an afterthought. In fact, it is the single biggest lever on performance. It decides whether an algorithm finishes in milliseconds or grinds to a halt. A graph is an abstract mathematical object. It is a set of vertices connected by edges. But a computer needs a concrete data layout to work with it. Two representations dominate practice: the adjacency matrix, a grid recording every possible pair of vertices, and the adjacency list, a collection of per-vertex lists recording only the edges that actually exist. They are logically equivalent — both fully describe the same graph. But they have wildly different space footprints. They also have different costs for the operations algorithms actually perform. Examples include "list all neighbors of v" or "is there an edge from u to v?" This distinction is not academic bookkeeping. Road-network routers, compilers building dependency graphs, dating-app matching engines, and web crawlers all choose one representation over the other. Their choice depends on how dense their graphs are. It also depends on which queries dominate. Getting it wrong has costs. Either you waste gigabytes of memory on a mostly-empty grid, or you pay a linear-time penalty. That penalty hits every time a neighbor-existence check is needed. This article builds both representations from first principles. It derives their exact space and time costs. It gives you the decision rule working engineers actually use. It sets up the data layer that later traversal algorithms will operate on. These include Breadth-First Search and Depth-First Search, which build directly on top of it.

Key takeaways
  • A graph G=(V,E)G = (V, E) can be stored as an adjacency matrix (∣V∣2|V|^2 grid) or an adjacency list (per-vertex neighbor lists).
  • Adjacency matrices give O(1)O(1) edge lookup but cost Θ(V2)\Theta(V^2) space regardless of how many edges exist.
  • Adjacency lists cost Θ(V+E)\Theta(V + E) space and are the standard choice for sparse graphs, where E≪V2E \ll V^2.
  • Listing all neighbors of a vertex is O(V)O(V) with a matrix but O(deg⁡(v))O(\deg(v)) with a list — a huge gap in sparse graphs.
  • Directed graphs store each edge once; undirected graphs are typically stored as if each edge were two directed edges.
  • The representation choice directly determines the running time of downstream algorithms like Breadth-First Search and Depth-First Search.
The core insight

Choose the adjacency list for sparse graphs and fast neighbor iteration, and the adjacency matrix for dense graphs or fast edge-existence queries — the two representations trade space and query speed in exactly opposite directions.

The idea

A graph G=(V,E)G = (V, E) consists of a vertex set VV and an edge set E⊆V×VE \subseteq V \times V. Let n=∣V∣n = |V| and m=∣E∣m = |E|. The graph may be directed, where an edge (u,v)(u, v) points from uu to vv only, or undirected, where an edge {u,v}\{u, v\} is symmetric. A representation is simply a concrete data layout that answers two canonical queries that essentially every graph algorithm needs: enumerate the neighbors of a given vertex, and test whether a specific edge exists. The adjacency matrix representation fixes an ordering v1,…,vnv_1, \dots, v_n of the vertices and builds an n×nn \times n boolean (or weighted) matrix AA, where

A[i][j]={1if (vi,vj)∈E0otherwiseA[i][j] = \begin{cases} 1 & \text{if } (v_i, v_j) \in E \\ 0 & \text{otherwise} \end{cases}

For a weighted graph, A[i][j]A[i][j] stores the edge weight instead of 11, with a sentinel (such as ∞\infty or a special "no edge" flag) in place of 00. For an undirected graph the matrix is symmetric, A[i][j]=A[j][i]A[i][j] = A[j][i], so only the upper triangle carries independent information, though implementations usually still store the full square for uniform indexing. The adjacency list representation instead keeps, for each vertex vv, an explicit list Adj[v]\text{Adj}[v] containing only the vertices uu such that (v,u)∈E(v, u) \in E. This can be implemented as an array of linked lists, an array of dynamic arrays, or — in performance-critical systems — a single flattened array with an index array marking where each vertex's neighbor block starts (the CSR, compressed sparse row, layout used in graph-analytics engines). The key structural fact is that an adjacency list only ever stores real edges: a vertex with no neighbors costs a single empty list, not nn wasted matrix cells.

How it works

Both representations are built by the same conceptual procedure — read each edge once and record it in the appropriate structure — but they diverge in what gets written and how much space is reserved up front.

Building an adjacency matrix. Allocate the full n×nn \times n grid initialized to "no edge," then for each edge write into the matching cell(s). ``` BUILD-ADJ-MATRIX(V, E, directed) n = |V| allocate A[1..n][1..n], initialize every A[i][j] = 0 for each edge (u, v) in E A[u][v] = 1 // or weight(u, v) for weighted graphs if not directed A[v][u] = 1 // mirror the edge for symmetry return A ```

Building an adjacency list. Allocate nn empty lists, then append each endpoint to its partner's list. ``` BUILD-ADJ-LIST(V, E, directed) n = |V| allocate Adj[1..n], each an empty list for each edge (u, v) in E APPEND(Adj[u], v) if not directed APPEND(Adj[v], u) // undirected edge appears in both lists return Adj ```

Core queries, both representations. ``` HAS-EDGE-MATRIX(A, u, v) return A[u][v] != 0 // O(1) HAS-EDGE-LIST(Adj, u, v) for each w in Adj[u] // O(deg(u)) worst case if w == v return TRUE return FALSE NEIGHBORS-MATRIX(A, u, n) result = empty list for j = 1 to n // always scans n cells, even if no edges exist if A[u][j] != 0 APPEND(result, j) return result NEIGHBORS-LIST(Adj, u) return Adj[u] // O(deg(u)), no wasted work ``` The essential asymmetry is visible immediately: `NEIGHBORS-MATRIX` walks an entire row of length nn no matter how many real edges uu has, whereas `NEIGHBORS-LIST` touches exactly deg⁡(u)\deg(u) cells and nothing more. That single difference is what makes traversal algorithms like Breadth-First Search and Depth-First Search run in O(V+E)O(V + E) time on adjacency lists but O(V2)O(V^2) time on adjacency matrices.

Figure
Same graph, two representations: adjacency list vs. adjacency matrix
Directed graph G with V = {A, B, C, D}, E = {(A→B),(A→C),(B→C),(C→D)} A B C D Adjacency List — Θ(V + E) = Θ(4 + 4) Adj[A] → [ B, C ] Adj[B] → [ C ] Adj[C] → [ D ] Adj[D] → [ ] Adjacency Matrix — Θ(V²) = Θ(16 cells) A B C D A [ 0 1 1 0 ] B [ 0 0 1 0 ] C [ 0 0 0 1 ] D [ 0 0 0 0 ] List stores exactly the 4 real edges. Matrix reserves all 16 cells, 12 of them wasted "0" entries.

Complexity & correctness

Space. The adjacency matrix always allocates n2n^2 cells, giving Θ(V2)\Theta(V^2) space regardless of mm. The adjacency list stores an array of nn list-heads plus one entry per directed edge — for an undirected graph, each edge contributes two entries (one in each endpoint's list) — giving Θ(V+E)\Theta(V + E) space. When the graph is sparse, meaning m=O(V)m = O(V) or more generally m=o(V2)m = o(V^2), the list is asymptotically smaller. When the graph is dense, with m=Θ(V2)m = \Theta(V^2) (a constant fraction of all possible edges present), the two become the same order, and the matrix's per-cell overhead is often lower in practice because it avoids pointer storage.

Time for the two canonical queries.

OperationAdjacency MatrixAdjacency List
Test edge (u,v)(u,v)Θ(1)\Theta(1)O(deg⁡(u))O(\deg(u)) worst case
Enumerate neighbors of vvΘ(V)\Theta(V) alwaysΘ(deg⁡(v))\Theta(\deg(v))
Add an edgeΘ(1)\Theta(1)Θ(1)\Theta(1) amortized (append)
Remove edge (u,v)(u,v)Θ(1)\Theta(1)O(deg⁡(u))O(\deg(u)) to find, then Θ(1)\Theta(1) to unlink
Build from edge listΘ(V2+E)\Theta(V^2 + E)Θ(V+E)\Theta(V + E)
Total spaceΘ(V2)\Theta(V^2)Θ(V+E)\Theta(V + E)

The reasoning behind each entry is direct from the pseudocode above, not incidental:

  • Edge test on a matrix is Θ(1)\Theta(1) because it is a single array dereference A[u][v]A[u][v] — no search is involved by construction.
  • Edge test on a list is O(deg⁡(u))O(\deg(u)) because in the worst case (edge absent, or it is the last entry) every neighbor of uu must be inspected before concluding.
  • Neighbor enumeration on a matrix is always Θ(V)\Theta(V), even for an isolated vertex with zero neighbors, because the algorithm must scan an entire row to know which cells are nonzero. This is the crucial asymmetry: the matrix pays for the absence of edges as much as their presence.
  • Neighbor enumeration on a list is Θ(deg⁡(v))\Theta(\deg(v)) because the list contains nothing but real neighbors — no wasted scanning.

Correctness. Both structures are correct representations of GG in the following precise sense: for every ordered pair (u,v)∈V×V(u, v) \in V \times V, (u,v)∈E  ⟺  A[u][v]≠0  ⟺  v∈Adj[u](u,v) \in E \iff A[u][v] \neq 0 \iff v \in \text{Adj}[u]. This equivalence is maintained as an invariant by the construction procedures: `BUILD-ADJ-MATRIX` and `BUILD-ADJ-LIST` both iterate over every edge in EE exactly once (twice for undirected graphs, once per endpoint) and record it in the corresponding structure, and neither structure is ever asked to record an edge not in EE. Since the invariant holds after processing each edge and holds trivially before any edges are processed (empty matrix / empty lists correctly represent the empty edge set), it holds after all edges are processed — a simple loop invariant argument.

The honest tradeoff. There is no universally superior representation. A matrix wins when edge-existence queries dominate and the graph is dense (e.g., a complete-ish graph of pairwise similarities, or a small graph where V2V^2 comfortably fits in cache). A list wins when the graph is sparse and neighbor iteration dominates, which describes the overwhelming majority of real-world graphs — road networks, social graphs, web graphs, dependency graphs — where each vertex touches only a handful of others out of millions possible. A further practical note: for weighted graphs the matrix generalizes trivially (store weights instead of booleans), while the list must pair each neighbor with a weight, e.g. `Adj[u] = [(v1, w1), (v2, w2), ...]`, adding a constant-factor but not asymptotic cost.

Worked example

Consider a small directed graph representing a course-prerequisite dependency: V={V = \{ CS101, CS102, CS201, CS301, MATH101 }\}, with edges

E={(CS101,CS102), (CS101,CS201), (CS102,CS201), (CS201,CS301), (MATH101,CS201)}E = \{(\text{CS101}, \text{CS102}),\ (\text{CS101}, \text{CS201}),\ (\text{CS102}, \text{CS201}),\ (\text{CS201}, \text{CS301}),\ (\text{MATH101}, \text{CS201})\}

Here n=5n = 5, m=5m = 5. Trace `BUILD-ADJ-LIST` and `BUILD-ADJ-MATRIX` edge by edge (order: CS101 < CS102 < CS201 < CS301 < MATH101 for matrix indexing).

StepEdge processedAdjacency list updateMatrix cell set to 1
1(CS101, CS102)Adj[CS101] = [CS102]A[CS101][CS102]
2(CS101, CS201)Adj[CS101] = [CS102, CS201]A[CS101][CS201]
3(CS102, CS201)Adj[CS102] = [CS201]A[CS102][CS201]
4(CS201, CS301)Adj[CS201] = [CS301]A[CS201][CS301]
5(MATH101, CS201)Adj[MATH101] = [CS201]A[MATH101][CS201]

Final adjacency list (only real edges stored, total entries = 5 = mm): ``` Adj[CS101] = [CS102, CS201] Adj[CS102] = [CS201] Adj[CS201] = [CS301] Adj[CS301] = [] Adj[MATH101] = [CS201] ``` Final adjacency matrix (25 cells allocated, only 5 are 1 — 20 wasted zero-cells): ``` CS101 CS102 CS201 CS301 MATH101 CS101 [ 0 1 1 0 0 ] CS102 [ 0 0 1 0 0 ] CS201 [ 0 0 0 1 0 ] CS301 [ 0 0 0 0 0 ] MATH101 [ 0 0 1 0 0 ] ``` Now run the query "what must a student complete before taking CS201?" — i.e., enumerate the in-neighbors, or equivalently for outgoing-edge lists, check every vertex's list for CS201.

  • With the list, if we also maintained a reverse adjacency list `RevAdj[CS201]`, the answer is a direct Θ(deg⁡−(CS201))\Theta(\deg^-(\text{CS201})) read: `[CS101, CS102, MATH101]`, 3 steps.
  • With the matrix, scanning column CS201 costs Θ(n)=5\Theta(n) = 5 cell reads regardless of the true in-degree — here it happens to find 3 hits after touching 5 cells, but on a graph with n=106n = 10^6 and the same in-degree of 3, the matrix would still touch all 10610^6 cells while the reverse list would touch exactly 3.

This is the concrete face of the asymptotic gap derived above: as nn grows with mm staying proportionally small (sparse graphs are the realistic case for dependency graphs, social networks, and road maps), the list's cost tracks the true answer size while the matrix's cost tracks the irrelevant universe size nn.

Common pitfalls & traps

PitfallWhy it happensConsequence
Forgetting to mirror edges for undirected graphsOnly writing `A[u][v] = 1` or `Adj[u].append(v)` and skipping the symmetric writeTraversals miss edges when starting from vv toward uu; BFS/DFS silently produce wrong reachability
Using a matrix for a huge sparse graphAssuming Θ(1)\Theta(1) edge lookup is "always better" without checking density10610^6 vertices → 101210^{12} cells, exhausting memory instantly
Treating adjacency-list edge lookup as O(1)O(1)Confusing list append cost with list search costUnderestimating the true cost of repeated `HAS-EDGE-LIST` calls, e.g., inside a naive cycle-check
Double-counting edges in undirected list-based edge countsSumming ∑vdeg⁡(v)\sum_v \deg(v) and forgetting each undirected edge is counted twiceOff-by-factor-of-2 error in reported ∣E∣|E|
Ignoring self-loops and parallel edgesAssuming A[v][v]A[v][v] is always 0, or that a list never repeats a neighborSome algorithms (e.g. multigraphs, Eulerian-path checks) require explicit self-loop / multiplicity handling

Pioneers & history

The adjacency matrix traces its lineage to the birth of graph theory itself, where graphs were studied as combinatorial objects long before computers existed, and matrix representations of relations were standard tools in linear algebra and combinatorics. The adjacency list, by contrast, is fundamentally a computational idea — it only becomes the natural choice once one is reasoning about memory and running time on an actual machine, which places its systematic use squarely in the era of algorithm design as a formal discipline in the 1950s–70s.

Origins · 1 minClaude Berge and how this was discovered
CB
Claude Berge
1926–2002 · Pioneered the systematic mathematical theory of graphs, formalizing adjacency structures and connectivity that underlie both representations discussed here

History
1736
— Euler's solution to the Königsburg bridges problem originates graph theory, implicitly using an incidence structure between vertices and edges.
1950
s — Matrix representations of graphs (adjacency and incidence matrices) become standard tools in combinatorics and early network analysis.
1959
— Dijkstra's shortest-path algorithm is published, implicitly relying on efficient neighbor enumeration — a design pressure that favors list-like structures for sparse graphs.
1970
s — Systematic algorithm-analysis work (Aho, Hopcroft, Ullman; Knuth) formalizes the space/time tradeoff between adjacency lists and matrices as a standard topic in algorithm design.
1990
s–2000s — Large-scale sparse graphs (web graphs, social networks) push practice firmly toward adjacency-list variants like compressed sparse row (CSR) for graphs with billions of edges.

Where this matters

Search engines and web crawlers store the link graph of the web — trillions of potential vertex pairs but only a sparse fraction realized as actual hyperlinks — using adjacency-list-style structures (often the CSR format) because an adjacency matrix would be astronomically larger than all the world's storage combined. Road-network routing systems (GPS navigation, ride-sharing dispatch) represent intersections and roads as sparse adjacency lists, since each intersection connects to only a handful of others regardless of how many total intersections exist in a country. Social networks represent friendships or follows as adjacency lists for the same reason — most users are connected to a tiny fraction of the whole user base. Compilers and build systems (Makefiles, package managers) represent dependency graphs between modules as adjacency lists to answer "what does this module depend on" without scanning irrelevant modules. Dense-graph cases do appear — small graphs in computational biology comparing all pairs of a few hundred proteins, or fully-connected similarity graphs in clustering — and there the adjacency matrix's constant-time lookups and cache-friendly row scans are genuinely the better engineering choice. This foundational representation choice is the substrate on which Breadth-First Search, Depth-First Search, and virtually every other graph algorithm is implemented, since their stated running times of O(V+E)O(V + E) are only achievable when the underlying representation is an adjacency list.

Ask the tutor: How would the space and time tradeoffs change if the graph is stored using a hybrid structure, like a hash set of edges per vertex, instead of a plain list or matrix?

Why do we say adjacency lists cost Θ(V + E) instead of just Θ(E)?

The Θ(V)\Theta(V) term accounts for the array (or table) of list-heads: even a vertex with zero edges still needs a slot to hold its (empty) list, and there are n=∣V∣n = |V| such slots regardless of how many edges exist. The Θ(E)\Theta(E) term (or Θ(2E)\Theta(2E) for undirected graphs, since each edge is recorded at both endpoints) accounts for the actual neighbor entries. If a graph has many isolated or low-degree vertices, the Θ(V)\Theta(V) term can dominate, which is why both terms must appear in the bound rather than dropping the vertex count.

Is it ever correct to say adjacency matrices are strictly worse than adjacency lists?

No — that would ignore density. When a graph is dense, meaning mm is close to V2V^2, both structures use Θ(V2)\Theta(V^2) space, and the matrix often wins on constant factors because it avoids pointer or list-node overhead and gives excellent cache locality for row scans. The matrix also gives genuinely Θ(1)\Theta(1) edge-existence tests with no worst-case search, which some algorithms (e.g., all-pairs computations, or dense graph coloring heuristics) exploit directly. The list only dominates asymptotically once the graph is meaningfully sparse.

How do weighted graphs change either representation?

For a matrix, replace the boolean entry with the edge weight, using a sentinel like ∞\infty (or a special flag) to mean "no edge," since 00 might be a legitimate weight. This costs no extra asymptotic space. For a list, each neighbor entry becomes a pair `(neighbor, weight)` instead of a bare vertex id, which adds a constant factor to storage per edge but keeps the Θ(V+E)\Theta(V+E) asymptotic bound intact. Algorithms like Dijkstra's shortest path rely on exactly this weighted adjacency-list layout to efficiently retrieve a vertex's outgoing edges together with their costs.

What is the compressed sparse row (CSR) format and how does it relate to adjacency lists?

CSR is a memory-efficient, cache-friendly implementation of the adjacency-list idea using flat arrays instead of linked lists. One array stores all neighbor entries concatenated vertex by vertex; a second, shorter index array stores the starting offset of each vertex's block within the first array. This avoids per-node pointer overhead and gives contiguous memory access when scanning a vertex's neighbors, which matters enormously for cache performance on graphs with billions of edges, while preserving the same Θ(V+E)\Theta(V+E) asymptotic space as a pointer-based adjacency list.

Does the choice of representation change what BFS or DFS compute, or only how fast they run?

Only how fast they run, not what they compute. Both Breadth-First Search and Depth-First Search visit exactly the same set of reachable vertices and, given consistent tie-breaking, produce equivalent traversal structures regardless of representation, because both structures faithfully encode the identical edge set EE. The difference is purely computational cost: implemented over an adjacency list these algorithms run in Θ(V+E)\Theta(V + E) time, but implemented over an adjacency matrix they run in Θ(V2)\Theta(V^2) time, since each vertex expansion requires scanning a full row of length VV to find its neighbors.

How do I count edges correctly from an adjacency list to avoid the double-counting trap?

For a directed graph, ∣E∣|E| equals the total number of entries across all lists, i.e. ∑v∣Adj[v]∣\sum_v |\text{Adj}[v]|, with no adjustment needed. For an undirected graph, each true edge {u,v}\{u,v\} is stored twice — once in `Adj[u]` and once in `Adj[v]` — so the correct edge count is 12∑v∣Adj[v]∣=12∑vdeg⁡(v)\frac{1}{2}\sum_v |\text{Adj}[v]| = \frac{1}{2}\sum_v \deg(v), the classical handshake-lemma relation. Forgetting the factor of 12\frac{1}{2} is one of the most common bugs when computing graph statistics directly from an adjacency-list structure.

What about self-loops and multi-edges — do these representations handle them?

An adjacency matrix handles a self-loop naturally as a nonzero diagonal entry A[v][v]A[v][v], but it cannot represent a multi-edge (multiple parallel edges between the same pair) without switching from a boolean/weight cell to an integer count. An adjacency list handles both trivially: a self-loop is simply vv appearing in `Adj[v]`, and a multi-edge is simply the same neighbor appearing multiple times in the list. If your application involves multigraphs, the list representation is usually simpler to extend correctly.

Where this leads