Graphs

Topics in this section
2 cards

Graphs: nodes and edges, any shape

A graph is a set of nodes (vertices) and edges connecting pairs of them, with no rule about parents. Edges can be undirected (a friendship: both ways) or directed (a follow, a one-way street, 'A depends on B'). Edges can carry a weight: a distance, a price, a time.

A graph can contain cycles: you can leave a node and come back to it. Trees are the special case of a connected graph with no cycles. Once you see data as a graph, a family of questions opens up: is there a path from A to B, what's the shortest one, which nodes are reachable, where are the clusters.

An undirected graph of friends and a directed weighted graph of cities with distances
In real life

Social networks (people and connections), maps (junctions and roads with distances), the web (pages and links), payments between accounts, dependencies between services.

Check yourself

A map app stores one-way streets with their travel times. What kind of graph is that?

Storing a graph: list or matrix

There are two standard ways to store a graph. An adjacency list keeps, for each node, the list of its neighbours. Memory is proportional to nodes plus edges, and visiting all neighbours of a node is fast. An adjacency matrix is an n × n table where cell [a][b] says whether there's an edge (or holds its weight). Checking one specific edge is O(1), but memory is n² whatever the number of edges.

Most real graphs are sparse: each node connects to a few others out of millions. A social network with 10 million users has 10¹⁴ matrix cells, almost all empty. So the adjacency list is the default; the matrix suits small or very dense graphs.

The same small graph as an adjacency list and as a 4 by 4 matrix
In real life

Friend lists in a social network are an adjacency list. A matrix of flight prices between 30 airports, where most pairs have a route, is a reasonable matrix.

In code: Python · Rust
from collections import defaultdict, deque
g = defaultdict(list)          # adjacency list
for a, b in [("A","B"), ("A","C"), ("B","D")]:
    g[a].append(b); g[b].append(a)   # undirected: both ways

def reachable(g, start):        # breadth-first search
    seen, q = {start}, deque([start])
    while q:
        for nb in g[q.popleft()]:
            if nb not in seen:
                seen.add(nb); q.append(nb)
    return seen
  • The search uses a queue (breadth-first) and a set of seen nodes; without the set, a cycle would loop forever.
  • For weighted graphs, store (neighbour, weight) pairs in each list.
Check yourself

A graph has 5 million users and each has about 200 connections. Why is an adjacency matrix a poor choice?