A graph has a set of vertices and edges between them. Edges may be directed or undirected, weighted or unweighted. The first decision in any graph problem is how to store the graph.
Adjacency list
For each vertex, store a list of its neighbors (and edge weights if any). Total space: . Iterating a vertex's neighbors is — exactly the work you'd want. Checking whether a specific edge exists is , which can be slow.
Adjacency matrix
A matrix where entry is 1 (or the weight) if there's an edge, 0 (or ) otherwise. Total space: . Edge existence check is . Iterating a vertex's neighbors is — wasteful for sparse graphs.
Choosing between them
Use adjacency list for sparse graphs () and most general-purpose graph code. Use adjacency matrix when the graph is dense, when you need many edge-existence queries, or when you're running matrix algorithms (Floyd-Warshall, matrix exponentiation for path counting).
Edge list
A flat list of (u, v, weight) tuples. Useless for traversal but convenient for algorithms that process edges in sorted order (Kruskal's MST) or as input/output format.
Directed vs undirected
In an undirected graph, every edge shows up in both 's and 's adjacency list. In a directed graph, only on the source side. Be deliberate about this when reading input — bugs from "I forgot to add the reverse edge" are extremely common.
Implicit graphs
Sometimes the graph isn't stored at all — it's implicit in the problem state. A maze's grid is a graph (cells as vertices, walkable neighbors as edges). A puzzle's state space is a graph (positions as vertices, moves as edges). You compute neighbors on the fly inside your BFS/DFS rather than precomputing an adjacency list.