Skip to content
Work Practice Products Coding interviews Machine learning coding Quant developer HFT course Blog Method Why me About Enquire Book a call

Coding · Graphs

The adjacency list

Lesson · 8 min read · Graphs · Storing a graph

One list of neighbours per node: memory in proportion to the edges, and the representation almost every graph algorithm expects.

Structure

An adjacency list keeps, for each node, a list of the nodes it has an edge to. Nothing is stored for pairs that are not connected, so the memory is proportional to n + m rather than n².

0123stored as→01210232013312
The same four-node graph as in the matrix lesson. Each undirected edge appears in two lists.

Every graph traversal does the same thing at every node: "for each neighbour v of u". With a list that loop touches exactly the neighbours, so a full traversal costs O(n + m). With a matrix the same loop reads n entries per node, for O(n²) in total.

Weights, and nodes that are not numbers

For a weighted graph each entry becomes a pair (neighbour, weight).

  1. Step 1 / 2
    412730123stored as→01·42·113·221·332·7

    Entries written neighbour·weight: node 0 has edges to 1 (weight 4) and 2 (weight 1).

  2. Step 2 / 2
    AAPLMSFTNVDAAMDstored as→AAPLMSFTNVDAMSFTAAPLNVDANVDAMSFTAMDAAPLAMDNVDA

    A map from ticker to neighbours. An edge here could mean "returns correlated above some threshold".

When nodes have names (tickers, words, account numbers), use a hash map from name to list instead of an array. Nothing else in an algorithm changes: it still asks for the neighbours of a node.

Implementation

Almost every graph problem hands you an edge list and expects you to build the adjacency list first. This is the code to have in your fingers:

from collections import defaultdict

def build(n, edges, directed=False):
    adj = [[] for _ in range(n)]          # nodes 0..n-1
    for u, v in edges:
        adj[u].append(v)
        if not directed:
            adj[v].append(u)              # the line most often forgotten
    return adj

def build_weighted(edges):
    adj = defaultdict(list)               # nodes with any hashable label
    for u, v, w in edges:
        adj[u].append((v, w))
    return adj

Converting a matrix to a list is a matter of reading each row. The problem at the end of this module asks for exactly that; here is its solution in all six languages.

def matrix_to_list(matrix):
    # Row i lists the edges leaving i; reading it left to right keeps them sorted.
    return [[j for j, x in enumerate(row) if x == 1] for row in matrix]

The same code is the reference solution to Adjacency matrix to adjacency list, where it is compiled and run against the tests in all six languages.

Matrix or list

The two representations trade the same costs in opposite directions. A third, the plain list of edges, is what problems usually give you; it is the right structure for Kruskal's algorithm and Bellman–Ford, which only ever loop over all edges.

OperationMatrixAdjacency listEdge list
Is there an edge u → v?O(1)O(deg u)O(m)
List the neighbours of uO(n)O(deg u)O(m)
Visit every edgeO(n²)O(n + m)O(m)
MemoryO(n²)O(n + m)O(m)
Best fordense, small n, Floyd–Warshalltraversals, Dijkstra, almost everythingKruskal, Bellman–Ford
TimeO(n + m) to build and to traverse
SpaceO(n + m)

If you need fast "is there an edge?" checks as well, store each node's neighbours in a hash set instead of a list: O(1) checks on average, still O(n + m) memory.

Default to the adjacency list. Reach for the matrix only when n is small and the graph is dense, or when the algorithm is built on it.