Graphs

Lesson 1 of 8

Graph Representations

"The origins of graph theory are humble, even frivolous." — Norman Biggs, E. Keith Lloyd & Robin Wilson, Graph Theory 1736–1936

A graph is the most honest data structure in computer science, because it admits what the others hide: that the interesting part of your data is rarely the things — it is the connections between them. Friends who know each other, cities joined by roads, tasks that must run before other tasks, web pages that link to one another. Once you learn to see graphs, you see them everywhere, and a huge share of the hardest, highest-signal interview questions collapse into "model this as a graph, then walk it." But before you can walk a graph, you have to store one — and how you store it quietly decides which algorithms are fast and which never finish.

The question this chapter answers

You are handed a pile of connections — friendships, roads, dependencies. How do you put that in memory so that the questions you'll ask next ("who are my neighbors?", "is there an edge here?") are cheap instead of catastrophic?

Core Concepts

A graph is two sets: vertices (the things, also called nodes) and edges (the connections between pairs of them). That is the whole definition — everything else is vocabulary describing what kind of graph you have, and interviewers expect you to speak it fluently:

  • Directed vs undirected — an edge either points one way (Twitter "follows") or both (Facebook "friends"). In an undirected graph, an edge {u, v} means u connects to v and v to u.
  • Weighted vs unweighted — each edge may carry a number: a road's distance, a flight's price, a link's strength. Unweighted just means every edge costs 1.
  • Cyclic vs acyclic — a cycle is a path that returns to where it started. A graph with no cycles is acyclic; a directed acyclic graph (a DAG) is the shape of every dependency system.
  • Dense vs sparse — dense means edges near the maximum (~V²); sparse means far fewer (closer to V). Most real graphs — social networks, road maps — are sparse, and that single fact drives the representation you'll pick.
  • Degree — how many edges touch a vertex. A self-loop is an edge from a vertex to itself; a multigraph allows more than one edge between the same pair. A connected component is a maximal group of vertices all reachable from one another.
Everyday analogy

A graph is a map of a city's flights. Airports are vertices; routes are edges. A route from Lagos to London that has no return leg is directed; the ticket price is a weight; an airport's number of routes is its degree. Most airports connect to only a handful of others out of thousands worldwide — that is what sparse means, and it is why nobody stores flights in a giant thousands-by-thousands grid of mostly-empty cells.

And a callback to Volume 5: a tree is just a connected, acyclic, undirected graph — V vertices and exactly V − 1 edges, no cycles. Everything you learned about traversing trees is graph traversal with training wheels; graphs remove the wheels by allowing cycles and multiple paths, which is why we'll spend the next chapter learning to avoid walking in circles.

Representation 1 — the adjacency list (your default)

An adjacency list stores, for each vertex, a list of its neighbors — a Map (or array) from node to its connections. It is the representation you reach for in ~90% of interviews because it matches how real graphs are shaped: sparse.

JavaScript — Try it Yourself
Loading editor…
Console
Runs automatically. Edit the code to see it change.

Notice the difference the two versions produce for vertex C: directed leaves it a sink with no outgoing edges ([]), while undirected records that C connects back to both A and B. Space is O(V + E) — you store each vertex once and each edge once (or twice, undirected). Listing a vertex's neighbors is O(degree) — you touch exactly the edges that exist and not one cell more. That efficiency on sparse graphs is the entire case for the adjacency list.

Representation 2 — the adjacency matrix

An adjacency matrix is a V × V grid where matrix[i][j] is 1 (or the edge's weight) when an edge runs from i to j, and 0 otherwise. It trades memory for a superpower: checking whether a specific edge exists is O(1).

JavaScript — Try it Yourself
Loading editor…
Console
Runs automatically. Edit the code to see it change.

An undirected matrix is symmetric across its diagonal (m[i][j] === m[j][i]) — a quick correctness check. The catch is that O(V²) space: for 100,000 vertices that is ten billion cells, almost all of them zeros, for a graph that an adjacency list would hold in a few megabytes. Reach for the matrix only when the graph is dense, or when you'll ask "does edge X exist?" far more often than "who are X's neighbors?".

The engineer's notebook

How experienced engineers choose a representation, in seconds:

• Default to the adjacency list. Real graphs are sparse, and O(V + E) space with O(degree) neighbor iteration is what every traversal wants.
• Reach for the matrix only when V is small or the graph is dense, or when constant-time edge lookups dominate the workload (e.g. Floyd–Warshall shortest paths).
• Expect the edge list as input. Most problems hand you [[u, v], ...]; converting it to an adjacency list is your reflexive first line.
• Watch the indexing. LeetCode-style problems are 0-indexed; textbook graphs are often 1-indexed. Pick one and size your arrays accordingly.

Representation 3 — the edge list

An edge list is simply an array of [u, v] (or [u, v, weight]) pairs. It is the most compact form and, crucially, the format problems most often give you. On its own it answers questions slowly — checking a specific edge means scanning all E of them — so you rarely compute on it directly. Two big exceptions: it is the natural input to Kruskal's minimum-spanning-tree algorithm and to union-find, both of which process edges one at a time (you'll meet them later in this Part).

Converting an edge list into an adjacency list

Because problems arrive as edge lists but algorithms want adjacency lists, this conversion is the near-universal first step of a graph solution. Burn it into muscle memory:

JavaScript — Try it Yourself
Loading editor…
Console
Runs automatically. Edit the code to see it change.

Initializing every vertex's list before the loop (the Array.from line) is the small discipline that prevents the single most common graph bug — pushing onto undefined.

Weighted graphs and grids-as-graphs

For a weighted graph, store [neighbor, weight] pairs instead of bare neighbors — the same adjacency list, one field richer. Dijkstra and friends read exactly this shape.

JavaScript — Try it Yourself
Loading editor…
Console
Runs automatically. Edit the code to see it change.

The other shape you'll meet constantly is a grid — a matrix where each cell is a vertex and its neighbors are the cells up, down, left, and right. You almost never build an explicit adjacency list for a grid; instead you generate neighbors on the fly with a direction array. This trick appears in flood-fill, island-counting, and maze problems everywhere:

JavaScript — Try it Yourself
Loading editor…
Console
Runs automatically. Edit the code to see it change.

The bounds check before the array access is not optional — it is what keeps you from reading grid[-1] or grid[R] and crashing.

Here is the comparison that should live in your head when an interviewer asks "how would you store it?":

Operation Adjacency list Adjacency matrix Edge list
Space O(V + E) O(V²) O(E)
Add an edge O(1) O(1) O(1)
Check if edge (u, v) exists O(degree(u)) O(1) O(E)
Iterate a vertex's neighbors O(degree(u)) O(V) O(E)
Best for sparse graphs, traversal (the default) dense graphs, fast edge lookups input format, Kruskal / union-find
History changed here

Graph theory has a birthday: 1736, when Leonhard Euler was asked whether you could stroll the seven bridges of Königsberg crossing each exactly once. Euler's insight was to throw away the map entirely — the distances, the shapes, the land — and keep only what mattered: which landmasses connected to which. Reduced to vertices and edges, the answer fell out in a line (you can't; too many landmasses touch an odd number of bridges). That act of abstraction — discard everything but the connections — is exactly what you do every time you turn a messy problem into an adjacency list.

Real-world example — chika.ng Academy

The Academy's curriculum is a graph: each lesson is a vertex, and "you must finish X before Y" is a directed edge — a DAG of prerequisites. Stored as an adjacency list keyed by lesson ID (a natural fit for the Drizzle/MySQL rows the app already has), answering "what can this student unlock next?" is a cheap O(degree) walk of one node's neighbors. Had we chosen a matrix, every prerequisite check across hundreds of lessons would waste an O(V²) grid that is almost entirely zeros — because, like nearly every real graph, the curriculum is sparse.

Common Mistakes

  • Forgetting undirected edges go both ways. Adding only adj[u].push(v) for an undirected graph silently drops half your edges — traversals then miss vertices that are genuinely reachable. Add both directions, always.
  • Using a matrix for a huge sparse graph. A million-vertex social network in a V × V matrix asks for 10¹² cells and blows up memory instantly. Sparse graph → adjacency list, no exceptions.
  • Pushing before the list exists. adj.get(u).push(v) throws when u was never initialized. Either guard with if (!adj.has(u)) adj.set(u, []), or pre-size the array with Array.from({ length: n }, () => []).
  • Mixing 0- and 1-indexed vertices. A 1-indexed problem needs an array of length n + 1 (or you'll be off by one and read undefined). Decide the convention once, up front.
  • Ignoring sink nodes and isolated vertices. A vertex with no outgoing edges still exists; if you only create list entries when you see an outgoing edge, you'll lose it. Initialize both endpoints of every edge.
  • Assuming no self-loops or duplicate edges. Real inputs may contain [u, u] or repeated pairs (a multigraph). Know whether your problem allows them before you write code that assumes they can't happen.
Thought experiment

Picture a social network: 1,000,000 users, and the average user has 200 friends. As an adjacency list, you store roughly V + 2E = 1,000,000 + 400,000,000 entries — big, but ordinary; a few gigabytes. As an adjacency matrix, you store V² = 1,000,000,000,000 cells — a trillion, almost every one a zero, and no machine will hold it. Same graph, same friendships. The representation didn't change the data by one edge; it changed whether the program can exist at all. That is why "which representation?" is the first question, not an afterthought.

Interview Corner

Interview Corner — the same idea at three levels. Answer yours; recognise the one above it.
Junior
"What is a graph, and what's the difference between a directed and an undirected graph?"
Model answer — "A graph is a set of vertices plus edges connecting pairs of them. In a directed graph an edge points one way — like 'follows' on Twitter — so I store u -> v only. In an undirected graph the connection is mutual — like 'friends' — so I store both u -> v and v -> u."
Follow-up they'll throw: "How would you store it in code?" → An adjacency list: a Map or array from each vertex to a list of its neighbors.
Intermediate
"You're given n and an edge list. Build an adjacency list, and tell me the time and space complexity and how you'd handle weights."
Model answer — "I pre-size an array of n empty lists so every vertex — including isolated ones — has an entry, then loop the edges pushing v onto adj[u] (and u onto adj[v] if undirected). That's O(V + E) time and O(V + E) space. For weights I store [neighbor, weight] pairs instead of bare neighbors — same structure, one field richer, which is exactly what Dijkstra reads."
Follow-up: "When would an adjacency matrix be the better call?" → When the graph is dense or V is small, or when I need O(1) edge-existence checks more than I need to iterate neighbors.
Senior
"We model a 50-million-node recommendation graph. Walk me through the representation choice and where it breaks at scale."
Model answer — "It's sparse, so an adjacency list is the only viable in-memory shape — a matrix is 2.5 × 10¹⁵ cells, a non-starter. At 50M nodes I'd stop thinking of it as one array: I'd use compressed formats (CSR — a flat neighbor array plus offset index) for cache-friendly iteration, and past a single machine I'd partition the graph across shards, which turns neighbor lookups that cross partitions into a distributed-systems problem, not an array-indexing one. The representation you choose at small scale (adjacency list) is right; what changes is *where it lives* and how you cut it."
The senior move: name the standard structure, then show you know it stops being an array problem and becomes a systems problem — CSR, sharding, and the cost of edges that cross machine boundaries.
Company lens — how the same topic gets asked
micro1Live and conversational — narrate your model as you build it: "vertices are users, edges are friendships, undirected so I push both ways." State the O(V + E) as you type; thinking out loud beats silent perfection.
GoogleWants rigor on the trade-off: be ready to justify list vs matrix with exact space bounds and to name CSR or partitioning when they push on scale.
MetaFast and pattern-first — they expect the edge-list-to-adjacency-list conversion written cleanly from memory, because it's the opening move of nearly every graph problem they ask.
AmazonOperational framing: "what does this cost in memory at 100× the nodes?" Tie the representation to real resource limits, and connect it to a Leadership Principle like Frugality.
Hands-on exercise
  1. In the editor above, extend toAdjList to also return each vertex's degree. For a directed graph, return in-degree and out-degree separately — and say out loud why the two differ.
  2. Write a function hasEdge(adj, u, v) for the adjacency-list representation, then state its complexity. Compare it to the same lookup on an adjacency matrix and explain, in one sentence, the trade-off you just measured.
  3. Take the grid example and count its connected components of open (0) cells by hand, using the neighbors function's up/down/left/right rule. Then predict: which representation — implicit grid or explicit adjacency list — uses less memory here, and why?

Chapter Summary

  • A graph is vertices + edges; the vocabulary (directed/undirected, weighted/unweighted, cyclic/acyclic, dense/sparse, degree, components) is the language interviewers expect you to speak.
  • A tree is a connected, acyclic, undirected graph with exactly V − 1 edges — graphs are trees with cycles and multiple paths allowed.
  • The adjacency list (O(V + E) space, O(degree) neighbor iteration) is the default; it fits sparse real-world graphs and every traversal.
  • The adjacency matrix (O(V²) space, O(1) edge lookup) suits dense graphs and edge-existence queries — and blows up memory on large sparse ones.
  • The edge list (O(E) space) is the input format you'll usually receive and the natural shape for Kruskal and union-find.
  • Converting an edge list to an adjacency list is the reflexive first step of most graph problems — undirected adds both directions; pre-size every list to avoid pushing onto undefined.
  • Store weights as [neighbor, weight] pairs, and treat a grid as an implicit graph whose neighbors you generate with a direction array plus a bounds check.
Narrative bridge

You can now put a graph into memory and choose the shape that keeps your next questions cheap. But a stored graph just sits there — its value is unlocked only when you walk it: visiting vertices, following edges, discovering what connects to what. The most fundamental walk answers "what is the shortest number of hops from here to there?" and explores a graph in rings, level by level. That is Breadth-First Search, and it is where we go next.

Editorial reflection — Euler's genius at Königsberg was not solving the puzzle; it was realizing the map didn't matter — only the connections did. Every time you choose an adjacency list over a matrix, or turn a grid into up/down/left/right neighbors, you repeat his move: throw away everything except how the pieces connect. Master that habit and graphs stop being a scary interview category and become what they truly are — the most faithful model we have of a connected world.

Q&A · 0

Enrol to ask questions and join the discussion.

No questions yet — be the first to ask.