Choosing representation
How to choose between matrix, list, edge list and CSR: costs of the operations you need, density, and what Java's nested collections really cost.
Learning goals
- Compare time and memory of the representations for common operations
- Choose by the operations the algorithm performs most
- Decide with density in mind
- Avoid the overhead of boxed nested collections when it matters
Prerequisites
There is no best representation, only the best one for your operations. A search that visits every neighbour wants lists; a routine that asks 'is there an edge from a to b?' a million times wants a matrix or a hash set.
Time/memory complexity
Count what each representation stores and what each operation costs. The matrix stores n² cells; an undirected adjacency list stores 2m entries plus n list headers; an edge list stores m entries; CSR stores n + 1 + 2m ints. The next items refine these numbers by operation.
Counting cells, not bytes, is the portable way to compare designs: bytes depend on the JVM. For a graph with n = 1000 vertices and m = 3000 edges, the matrix has 1,000,000 cells while the list has 6,000 entries and CSR has 7,001 cells.
| Representation | Space (undirected) | Edge test | List neighbours | Add edge | Best for |
|---|---|---|---|---|---|
| Adjacency matrix | n² | O(1) | O(n) | O(1) | dense graphs, many edge tests |
| Adjacency list | n + 2m | O(degree) | O(degree) | O(1) | traversals, most algorithms |
| Edge list | m | O(m) | O(m) | O(1) | sorting edges, Kruskal, Bellman-Ford |
| CSR | n + 1 + 2m | O(degree) | O(degree), contiguous | rebuild | static graphs, speed |
1static long[] storedCells(int n, int m) { // {matrix, list entries, edge list, CSR} for an undirected graph2return new long[]{(long) n * n, 2L * m, m, n + 1L + 2L * m};3}
Edge lookup
'Is there an edge from a to b?' is O(1) in a matrix, O(degree of a) in a list and O(m) in an edge list. When the question is asked often on a sparse graph, keep a HashSet of edges encoded as one long (a × n + b) next to the list: O(1) average per test and only m entries. The set must be kept in step with the list when edges are added.
Encode with long, not int: for n above 46,340 the product a × n overflows int. The code shows the list scan and the set lookup side by side; they must always agree.
1class EdgeLookup {2private final List<List<Integer>> g = new ArrayList<>();3private final Set<Long> set = new HashSet<>();4private final int n;5EdgeLookup(int n) {6this.n = n;7for (int i = 0; i < n; i++) g.add(new ArrayList<>());8}9void addEdge(int a, int b) { // directed10g.get(a).add(b);11set.add((long) a * n + b); // long: a * n overflows int for large n12}13boolean viaList(int a, int b) { return g.get(a).contains(b); } // O(degree)14boolean viaSet(int a, int b) { return set.contains((long) a * n + b); } // O(1) average15}
Iteration cost
Algorithms mostly iterate over neighbours, so measure the cost of that. Visiting every neighbour of every vertex costs O(n²) with a matrix (a full row scan per vertex) and O(n + m) with lists or CSR. On a sparse graph that gap decides everything: for n = 1000 and m = 3000, a full traversal reads 1,000,000 matrix cells against 7,000 list entries.
The code counts steps rather than time, so the comparison is exact and independent of the machine: scanning one row of the matrix always takes n steps, scanning a list takes degree steps.
1static long traversalStepsMatrix(boolean[][] m) { // visit every neighbour of every vertex2long steps = 0;3for (int u = 0; u < m.length; u++)4for (int v = 0; v < m.length; v++) steps++; // the whole row is read, edge or not5return steps;6}7static long traversalStepsList(List<List<Integer>> g) {8long steps = g.size(); // one step per vertex ...9for (List<Integer> nb : g) steps += nb.size(); // ... plus one per neighbour entry10return steps;11}
Dense vs sparse
Use the matrix when the graph is dense (m close to n²) or small enough that n² cells fit comfortably, or when you need constant-time edge tests and weights between all pairs (Floyd-Warshall). Use adjacency lists or CSR when the graph is sparse, which is the usual case for large real inputs. A rough rule of thumb: if m is more than a few percent of n², a matrix is competitive; below that, lists win clearly.
The numbers are blunt: a matrix of int for n = 10,000 needs 10,000² × 4 bytes = 400 million bytes of payload, which is 400 MB, whether the graph has 10 edges or 10 million. The function below applies a simple heuristic; treat its threshold as a starting point to be tuned for your data, not as a law.
1static String suggest(int n, long m) { // a heuristic: matrix only when m is a sizeable fraction of n^22long possible = (long) n * n;3return m * 20 >= possible ? "matrix" : "adjacency list";4}
Java nested collection overhead
List<List<Integer>> is convenient but costly: each inner list is an object with its own header and backing array, and every Integer above the cached range (-128 to 127) is a separate heap object of its own, reached through a reference. A graph with a million edges can have millions of small objects, which costs memory and cache misses and gives the garbage collector work. Typical 64-bit JVM figures (16 bytes for an Integer, 4-8 for the reference, plus list headers) are estimates, not measurements; measure your own case.
When performance or memory matters, build an int[][] (one int[] per vertex, sized by degree) or CSR. The conversion below turns the nested lists into jagged arrays: the same neighbour order, no boxing, and far fewer objects. For most contest-sized graphs the lists are fine; reach for arrays when n or m reaches the millions.
1static int[][] toJagged(List<List<Integer>> g) { // one int[] per vertex, no boxing2int[][] out = new int[g.size()][];3for (int u = 0; u < g.size(); u++) {4List<Integer> nb = g.get(u);5out[u] = new int[nb.size()];6for (int i = 0; i < out[u].length; i++) out[u][i] = nb.get(i);7}8return out;9}10static int[][] jaggedFromEdges(int n, int[][] edges) { // undirected, built without boxed lists at all11int[] deg = new int[n];12for (int[] e : edges) { deg[e[0]]++; deg[e[1]]++; }13int[][] g = new int[n][];14for (int u = 0; u < n; u++) g[u] = new int[deg[u]];15int[] fill = new int[n];16for (int[] e : edges) { g[e[0]][fill[e[0]]++] = e[1]; g[e[1]][fill[e[1]]++] = e[0]; }17return g;18}
Practice ladder
Now try it yourself
Easy
- Find the Town Judge
Medium
- Clone Graph
- Number of Provinces
Advanced
- Shortest Path Visiting All Nodes
- Reconstruct Itinerary
Key takeaways
- Count cells: matrix n², list 2m + n, edge list m, CSR n + 1 + 2m.
- Traversals favour lists or CSR; frequent edge tests favour a matrix or a hash set.
- Boxed nested collections cost memory and cache misses; use int arrays or CSR for very large graphs.