datarekha

Graph Representations

How to store a graph in code — adjacency list versus adjacency matrix — and how to pick the right one for the job.

7 min read Intermediate Data Structures & Algorithms Lesson 20 of 32

What you'll learn

  • What a graph is, and why it appears everywhere — social networks, build systems, maps
  • The difference between directed and undirected graphs, and what 'degree' means
  • How an adjacency list stays O(V+E), which is why sparse graphs love it
  • How an adjacency matrix buys O(1) edge lookup at the price of O(V²) space

Before you start

A graph is the simplest structure for capturing relationships: just nodes (things) and edges (connections between them). Friendships on a social network, links between web pages, flights between airports — every one of these is a graph.

The question this lesson answers is not what a graph is, but how to store one so your code can work with it without drowning in memory or time.

A few words first

An edge can be directed (an arrow from A to B that does not imply B to A — a Twitter follow) or undirected (a mutual link — a Facebook friendship). It can also be weighted, carrying a number like distance or cost. A node’s degree is how many edges touch it. And graphs are usually sparse: a social network of a million people has nowhere near the trillion friendships a fully-connected graph would hold. That sparseness is the single fact that decides which representation to use.

Here is the small undirected graph we will store two ways:

ABCDE
Five nodes, six edges. A is adjacent to B and C; D is adjacent to B, C, and E.

Adjacency list

The everyday choice. For each node, you keep the list of its neighbours:

graph = {
    "A": ["B", "C"],
    "B": ["A", "D", "E"],
    "C": ["A", "D"],
    "D": ["B", "C", "E"],
    "E": ["B", "D"],
}

This costs O(V + E) space — one entry per node, plus one per edge end. Walking all of a node’s neighbours is O(degree), which is exactly what graph traversals do over and over. Checking whether a specific edge exists is O(degree) with a list (or O(1) if you store neighbours in a set). Adding an edge is O(1).

Adjacency matrix

The other choice is a V×V grid where cell [i][j] = 1 marks an edge from node i to node j. For an undirected graph it is symmetric:

     A  B  C  D  E
  A  0  1  1  0  0
  B  1  0  0  1  1
  C  1  0  0  1  0
  D  0  1  1  0  1
  E  0  1  0  1  0

The matrix’s one superpower is O(1) edge lookup — “is there an edge from C to D?” is a single cell read. But it always costs O(V²) space, whether the graph has six edges or six million, and walking a node’s neighbours means scanning a whole row of mostly zeros — O(V). With 10,000 nodes that is 100 million cells, regardless of how few edges you actually have.

Which one?

Your situationReach for
Sparse graph (E much smaller than V²)Adjacency list
Constant “is there an edge?” checksMatrix
BFS, DFS, Dijkstra on a large graphAdjacency list
V small and the graph denseEither
All-pairs algorithms (Floyd-Warshall)Matrix

The rule of thumb is short: start with an adjacency list. Almost every real graph — web, social, road, dependency — is sparse, so the matrix’s O(V²) is a memory tax you do not want to pay. Build one from an edge list and the cost difference shows up immediately:

def build_adj_list(edges):
    graph = {}
    for u, v in edges:
        graph.setdefault(u, []).append(v)
        graph.setdefault(v, []).append(u)      # undirected: store both ends
    return graph

edges = [("A","B"), ("A","C"), ("B","D"), ("B","E"), ("C","D"), ("D","E")]
graph = build_adj_list(edges)

V, E = len(graph), len(edges)
print("V =", V, " E =", E)
print("adjacency list  : V + E =", V + E, "entries")
print("matrix would be : V × V =", V * V, "cells")
V = 5  E = 6
adjacency list  : V + E = 11 entries
matrix would be : V × V = 25 cells

Eleven against twenty-five looks minor here. Scale to a million sparse nodes and it becomes the gap between megabytes and terabytes.

Practice

Quick check

0/3
Q1A social network has 50 million users, each with ~200 friends (E ≈ 5 billion). How many cells would an adjacency matrix need, and why is that impractical?
Q2You run Dijkstra on a large sparse road network (millions of nodes, ~3-4 roads each). Which representation, and why?
Q3In an undirected graph you add edge (A, B) to an adjacency list. How many list entries are added?

Sign in to track your progress

Completed lessons, your XP, level, and streak save to your account — it's free and takes a few seconds.

Practice this in an interview

All questions
Given a new data problem, how do you decide whether to use a list, dict, or set?

Choose a list when order matters and you need indexed access or duplicates. Choose a dict when you need to map keys to values and look up by key in O(1). Choose a set when you need uniqueness, fast membership testing, or set-algebra operations. Getting this choice wrong usually means either incorrect results (keeping duplicates when you needed uniqueness) or avoidable O(n) lookups.

How do you choose the right chart type for a given analytical question?

Match the chart to the relationship in the data: comparison across categories calls for bars, trends over continuous time call for lines, correlation between two numeric variables calls for a scatter plot, and distribution shape calls for a histogram or box plot. The question you are answering — not aesthetics — drives the choice.

How do state-space models like Mamba differ from attention, and when would you use one?

A state-space model carries a fixed-size hidden state forward through the sequence like a selective recurrence, giving O(N) time and constant per-step memory with no KV cache that grows with context. Attention instead compares every token to every other, which is O(N^2) but allows exact lookup of any past token. Mamba's gates are input-dependent, recovering much of attention's content-awareness; the trade-off is that a fixed state can't recall arbitrary far-back tokens as precisely. In practice, hybrids that interleave Mamba layers with a few attention layers give near-linear cost with near-attention quality.

When would you use a Python list versus a NumPy array, and what are the performance trade-offs?

Python lists are heterogeneous, pointer-based, and general-purpose. NumPy arrays are homogeneous, stored as contiguous typed memory, and support vectorised operations that run at C speed. For numerical work on more than a few hundred elements, NumPy is almost always faster and more memory-efficient.

Related lessons

Explore further

Skip to content