How can a graph be stored in memory?
Short answer
A graph in memory is usually stored as: 1) an adjacency matrix (O(V^2), fast edge checks), 2) an adjacency list (O(V+E), fast neighbor iteration, convenient additions/removals), 3) an edge list (O(E), simple to load and store), 4) compressed CSR/CSC structures for large sparse graphs. The choice depends on the graph's density, the required operations (edge check, neighbor traversal, updates), and memory constraints.
Detailed answer
Main storage methods
- Adjacency matrix: a two-dimensional V×V array. Memory O(V^2). Edge-existence check O(1). Neighbor iteration O(V). Good for dense graphs and frequent edge checks.
- Adjacency list: a list/set of neighbors for each vertex. Memory O(V+E). Edge check O(deg(u)) or O(1) with a Set. Neighbor iteration O(deg(u)). The best choice for sparse and dynamic graphs.
- Edge list: an array of pairs/triples (u, v, w). Memory O(E). Convenient for storage, serialization, and edge-based algorithms. Edge check O(E) (without indexing).
- CSR/CSC: compressed formats for sparse graphs. Memory close to O(V+E). Excellent neighbor iteration and cache-friendly traversal. Updating online is hard/expensive.
Adjacency Matrix
A two-dimensional V×V array, where element matrix[u][v] is 0/1 or the edge weight. For an undirected graph the matrix is symmetric.
- Memory: O(V^2). Inefficient for large sparse graphs.
- Edge check u→v: O(1). Neighbor iteration: O(V).
- Adding a vertex: O(V^2) (reallocation). Adding an edge: O(1).
- Suits dense graphs and tasks with frequent edge-existence checks.
const n = 5; // number of vertices
const matrix = Array.from({ length: n }, () => Array(n).fill(0));
function addEdge(u, v, w = 1, directed = false) {
matrix[u][v] = w; // 1 or the weight
if (!directed) matrix[v][u] = w;
}
function hasEdge(u, v) {
return matrix[u][v] !== 0;
}
function neighbors(u) {
const res = [];
for (let v = 0; v < n; v++) if (matrix[u][v] !== 0) res.push(v);
return res;
}
addEdge(0, 1);
addEdge(0, 3);
console.log(hasEdge(0, 1)); // true
console.log(neighbors(0)); // [1, 3]Adjacency List
For each vertex we keep a collection of its neighbors: an array, a Set, or a Map (for weighted graphs). Scales well for sparse graphs and supports dynamic changes.
- Memory: O(V+E).
- Edge check: O(deg(u)) with an array, O(1) with a Set/Map.
- Neighbor iteration: O(deg(u)). Adding/removing an edge: amortized O(1).
// Unweighted graph: Map<number, Set<number>>
const g = new Map();
function addVertex(u) {
if (!g.has(u)) g.set(u, new Set());
}
function addEdge(u, v, directed = false) {
addVertex(u); addVertex(v);
g.get(u).add(v);
if (!directed) g.get(v).add(u);
}
function hasEdge(u, v) {
return g.has(u) && g.get(u).has(v);
}
function neighbors(u) {
return g.get(u) ? [...g.get(u)] : [];
}
function removeEdge(u, v, directed = false) {
if (g.has(u)) g.get(u).delete(v);
if (!directed && g.has(v)) g.get(v).delete(u);
}
// Example usage
addEdge(0, 1);
addEdge(0, 2);
console.log(hasEdge(0, 1)); // true
console.log(neighbors(0)); // [1, 2]// Weighted graph: Map<number, Map<number, number>>
const wg = new Map();
function addWeightedEdge(u, v, w, directed = false) {
if (!wg.has(u)) wg.set(u, new Map());
if (!wg.has(v)) wg.set(v, new Map());
wg.get(u).set(v, w);
if (!directed) wg.get(v).set(u, w);
}
function weight(u, v) {
return wg.has(u) ? wg.get(u).get(v) : undefined; // undefined = no edge
}
addWeightedEdge(1, 3, 2.5);
console.log(weight(1, 3)); // 2.5Edge List
We store an array of edges, each edge a pair (u, v) or a triple (u, v, w). A simple format for data exchange and certain algorithms (for example, sorting by weight for Kruskal's).
- Memory: O(E).
- Edge check: O(E), without building indexes.
- Iterating over neighbors requires indexing/grouping.
// Edge list: [[u, v, w?]]
const edges = [ [0, 1], [0, 2], [2, 3] ];
// For fast neighbor access, group into an adjacency list on load
const adj = new Map();
for (const [u, v] of edges) {
if (!adj.has(u)) adj.set(u, []);
adj.get(u).push(v);
}
console.log(adj.get(0)); // [1, 2]CSR/CSC (Compressed Sparse Row/Column)
Compressed structures for sparse graphs (by analogy with sparse matrices). CSR stores all of a vertex's neighbors consecutively in a single array plus an offsets array for fast access.
- Memory: close to O(V+E), good memory locality.
- Ideal for traversals and computations on large graphs. Updates (adding/removing) are inconvenient, a rebuild is required.
// Building CSR from an edge list
function buildCSR(n, edges, directed = false) {
const tmp = Array.from({ length: n }, () => []);
for (const [u, v] of edges) {
tmp[u].push(v);
if (!directed) tmp[v].push(u);
}
const offsets = new Uint32Array(n + 1);
let total = 0;
for (let i = 0; i < n; i++) {
offsets[i] = total;
total += tmp[i].length;
}
offsets[n] = total;
const neighbors = new Uint32Array(total);
let idx = 0;
for (let i = 0; i < n; i++) {
for (const v of tmp[i]) neighbors[idx++] = v;
}
return { offsets, neighbors };
}
function neighborsCSR(csr, u) {
const { offsets, neighbors } = csr;
return neighbors.subarray(offsets[u], offsets[u + 1]);
}
const csr = buildCSR(5, [[0,1],[0,2],[2,3]]);
console.log([...neighborsCSR(csr, 0)]); // [1, 2]Incidence matrix and other variants
- Incidence matrix: V×E, useful for certain theoretical/linear-algebra problems. Memory O(V·E), rarely used in practice for large graphs.
- Object model (nodes/edges as objects with references): convenient for complex attributes, but more expensive in memory and cache.
Comparison by operations (averaged)
- Memory: matrix O(V^2) > adjacency list O(V+E) ≈ CSR O(V+E) > edge list O(E) (without indexes).
- Edge check u→v: matrix O(1), adjacency list O(deg(u)) or O(1) with a Set/Map, CSR O(log deg(u)) with binary search (if sorted) or O(deg(u)).
- Neighbor iteration: adjacency list/CSR O(deg(u)), faster and more cache-friendly, matrix O(V).
- Updates: adjacency list, cheap; matrix, cheap for edges, expensive for vertices; CSR, expensive (rebuild).
How to choose a structure
- Dense graph (many edges) with edge-existence checks that matter: adjacency matrix.
- Sparse graph, frequent traversals and updates: adjacency list (Set/Map).
- Very large sparse graph, few or no updates, traversal performance matters: CSR/CSC.
- Simple storage/exchange: edge list.
Specifics: directed, weighted, multigraphs
- Directed: store only u→v (do not duplicate v→u). In the matrix, elements are asymmetric.
- Weighted: numeric weights in the matrix, a Map<neighbor, weight> or an array of { to, w } objects in the adjacency list.
- Multigraphs: multiple edges are allowed. In the adjacency list use an array (not a Set), or keep a count/list of edges between a pair of vertices.
- Loops (u=u): supported by all structures; diagonal elements in the matrix.
Example: a typical graph for web tasks (BFS/DFS traversals)
// Adjacency list with a Set: convenient and fast for checks
const G = new Map();
const dir = false; // undirected
function v(u){ if(!G.has(u)) G.set(u, new Set()); }
function add(u,v){ v(u); v(v); G.get(u).add(v); if(!dir) G.get(v).add(u); }
// Build a small graph
add(0,1); add(0,2); add(1,3); add(2,3); add(3,4);
function bfs(start){
const q = [start], seen = new Set([start]), order = [];
while(q.length){
const u = q.shift(); order.push(u);
for(const v of G.get(u) || []) if(!seen.has(v)){ seen.add(v); q.push(v); }
}
return order;
}
function dfs(start){
const st = [start], seen = new Set(), order = [];
while(st.length){
const u = st.pop();
if(seen.has(u)) continue; seen.add(u); order.push(u);
for(const v of G.get(u) || []) if(!seen.has(v)) st.push(v);
}
return order;
}
console.log('BFS:', bfs(0)); // for example, [0,1,2,3,4]
console.log('DFS:', dfs(0));Summary
- There is no single correct representation: choose it for the task and the constraints.
- Matrix, for dense graphs and frequent edge checks; adjacency list, a universal and dynamic choice; edge list, a simple data format; CSR/CSC, for large sparse graphs and high-performance traversal.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.