"Before you can traverse a graph, you have to store it — and the two ways to store it make opposite trade-offs. Pick wrong and a million-node social network eats a trillion empty cells."
Adjacency List: Each Vertex Lists Its Neighbors
The most common representation: a dictionary (or list) mapping each vertex to a list of its neighbors. {"A": ["B", "C"], "B": ["A"], ...}. Space is O(V + E) — you store each vertex once and each edge once. To iterate a vertex's neighbors is fast and direct. To check "is there an edge A→B?" you scan A's neighbor list, O(degree). This is the right default for sparse graphs — graphs where the number of edges is far below the maximum possible (V²), which describes almost every real-world graph (you have hundreds of friends, not millions).
Adjacency Matrix: a Grid of Yes/No
The alternative: a V×V grid where cell [i][j] is 1 if there's an edge from i to j, else 0 (or the weight, if weighted). Edge lookup is O(1) — just index the cell. But space is O(V²) regardless of how few edges exist, and iterating one vertex's neighbors costs O(V) (scan its whole row). The matrix shines for dense graphs (many edges) or when you constantly ask "is there an edge between these two?", and it's a disaster for large sparse graphs — a million vertices would need a trillion cells, almost all zeros.
The Third Option, and Weights
A simpler third form is the edge list: just a list of (u, v) pairs (or (u, v, weight)). It carries no fast lookups, but some algorithms want exactly this — Kruskal's minimum spanning tree (next track) processes edges sorted by weight, so an edge list is the natural input. For weighted graphs, both main representations adapt easily: store (neighbor, weight) in the adjacency list, or put the weight in the matrix cell. Choosing the representation is part of solving the problem — the algorithm you'll run often dictates the form.