Skip to main content

Updated Sep 3, 2026

Graphs

A graph is a set of vertices and a set of edges connecting them. That is nearly no structure at all, which is precisely why it models so much: road networks, social connections, package dependencies, web links, state machines, and the call graph of the program you are reading this in.

Trees and linked lists are special cases — a tree is a connected graph with no cycles, a linked list a tree where every node has one child.

Six numbered vertices connected by undirected edges, with vertex 6 attached to the rest by a single edge and vertices 1, 2 and 5 forming a triangle
An undirected graph on six vertices. Note vertex 6 hanging off a single edge — removing it disconnects the graph, which makes it a bridge. Wikimedia Commons, Public domain

Core Concepts​

TermMeaning
Vertex (node)An entity
EdgeA relationship between two vertices
Directed / undirectedWhether edges have a direction (follower vs. friendship)
WeightedEdges carry a cost — distance, latency, price
DegreeNumber of edges at a vertex (in-degree/out-degree when directed)
PathA sequence of vertices joined by edges
CycleA path returning to its start
ConnectedEvery vertex reachable from every other
DAGDirected acyclic graph — directed, no cycles
Dense / sparseE close to V2V^2 / E close to V

DAGs deserve their own line​

A directed acyclic graph: several vertices joined by arrows, with no sequence of arrows leading back to a vertex already visited
A DAG. Because no path returns to where it started, the vertices can always be laid out so that every arrow points forward — that ordering is a topological sort. Wikimedia Commons, Public domain

Acyclicity is what makes dependency resolution, build systems, task scheduling and spreadsheet recalculation possible: it guarantees a valid order exists. A cycle in any of those is precisely the error condition ("circular dependency"). See Topological Sort.

Mechanism​

The two representations​

Adjacency list — each vertex stores its neighbours:

graph = {
1: [2, 5],
2: [1, 3, 5],
3: [2, 4],
4: [3, 5, 6],
5: [1, 2, 4],
6: [4],
}
# Weighted: store (neighbour, weight) pairs
weighted = {
1: [(2, 7), (5, 3)],
2: [(1, 7), (5, 1)],
# ... one entry per vertex, same shape as the unweighted graph above
}

Adjacency matrix — a V×V grid where m[i][j] marks an edge:

# 1 2 3 4 5 6
m = [[0, 1, 0, 0, 1, 0], # 1
[1, 0, 1, 0, 1, 0], # 2
[0, 1, 0, 1, 0, 0], # 3
[0, 0, 1, 0, 1, 1], # 4
[1, 1, 0, 1, 0, 0], # 5
[0, 0, 0, 1, 0, 0]] # 6
Adjacency listAdjacency matrix
SpaceO(V+E)O(V + E)O(V2)O(V^2)
Is there an edge u→v?O(degree(u))O(degree(u))O(1)O(1)
Iterate u's neighboursO(degree(u))O(degree(u))O(V)O(V) — scans empty cells too
Add an edgeO(1)O(1)O(1)O(1)
Best forSparse graphs — nearly all real onesDense graphs; matrix algorithms
Default to the adjacency list

Real graphs are overwhelmingly sparse. A social network with a million users and a hundred friends each has 10810^8 edges — an adjacency list holds that comfortably, while the matrix needs 101210^{12} cells, 99.99% of them zero. Reach for a matrix only when the graph is genuinely dense, or when an algorithm wants matrix form (Floyd–Warshall, spectral methods).

One weighted graph, three representations, traced​

Six vertices A B C D E F, nine weighted undirected edges: A–B 4, A–C 2, B–C 1, B–D 5, C–D 8, C–E 10, D–E 2, D–F 6, E–F 3. Building all three representations from the same edge list, with an approximate byte cost for a compact fixed-width layout (1 byte per vertex id, 4 bytes per integer weight, no per-object/pointer overhead — real language containers add more):

adjacency list (each undirected edge stored once per endpoint — 18 directed entries total)
A: [(B,4), (C,2)]
B: [(A,4), (C,1), (D,5)]
C: [(A,2), (B,1), (D,8), (E,10)]
D: [(B,5), (C,8), (E,2), (F,6)]
E: [(C,10), (D,2), (F,3)]
F: [(D,6), (E,3)]
cost: 18 entries x (1 byte id + 4 byte weight) = 90 bytes — O(V + E)

adjacency matrix (6x6, one 4-byte weight per cell, 0 meaning "no edge")
A B C D E F
A [ 0, 4, 2, 0, 0, 0]
B [ 4, 0, 1, 5, 0, 0]
C [ 2, 1, 0, 8, 10, 0]
D [ 0, 5, 8, 0, 2, 6]
E [ 0, 0, 10, 2, 0, 3]
F [ 0, 0, 0, 6, 3, 0]
cost: 36 cells x 4 bytes = 144 bytes — O(V^2)

edge list (each undirected edge stored exactly once)
[(A,B,4), (A,C,2), (B,C,1), (B,D,5), (C,D,8), (C,E,10), (D,E,2), (D,F,6), (E,F,3)]
cost: 9 entries x (2 x 1 byte id + 4 byte weight) = 54 bytes — O(E)

Three representations of the same nine edges span 90, 144 and 54 bytes here — a small enough graph that the difference looks minor, but the matrix's V2V^2 term dominates at scale exactly as the tip above argues. The edge list is smallest because it is the only one of the three that never repeats a fact: each edge appears once, where the other two either duplicate it (undirected list) or reserve space for every non-edge (matrix).

edges = [("A","B",4), ("A","C",2), ("B","C",1), ("B","D",5), ("C","D",8),
("C","E",10), ("D","E",2), ("D","F",6), ("E","F",3)]

adj = {v: [] for v in "ABCDEF"}
for u, v, w in edges:
adj[u].append((v, w))
adj[v].append((u, w))
assert adj["A"] == [("B", 4), ("C", 2)]
assert len(adj["C"]) == 4 # C touches A, B, D, E

idx = {v: i for i, v in enumerate("ABCDEF")}
matrix = [[0] * 6 for _ in range(6)]
for u, v, w in edges:
matrix[idx[u]][idx[v]] = w
matrix[idx[v]][idx[u]] = w
assert matrix[idx["A"]][idx["B"]] == 4
assert matrix[idx["D"]][idx["F"]] == 6

assert len(edges) == 9 # the edge list: exactly one entry per edge

Practical Usage​

# doc:no-run
from collections import defaultdict

# Building an undirected graph from an edge list
graph = defaultdict(list)
for u, v in edges:
graph[u].append(v)
graph[v].append(u) # omit this line for a directed graph

# Degree of a vertex
len(graph[v])
DomainVerticesEdgesQuestion asked
Maps / navigationIntersectionsRoads, weighted by timeShortest path
Social networksPeopleFriendships / followsDegrees of separation, communities
Package managersPackages"depends on"Topological order, cycle detection
CompilersBasic blocksControl flowReachability, dominance, dead code
NetworksRoutersLinks, weighted by costRouting
Web searchPagesHyperlinksPageRank

Edge Cases & Pitfalls​

  • Forgetting the reverse edge builds a directed graph when you wanted an undirected one, and the bug surfaces much later as an unreachable vertex.
  • Not tracking visited vertices turns any traversal of a cyclic graph into an infinite loop. This is the difference between graph traversal and tree traversal, and the most common graph bug there is.
  • Disconnected graphs. A traversal from one vertex reaches only its component. Finding all components means looping over every vertex and starting a traversal from each unvisited one.
  • Self-loops and parallel edges break assumptions in hand-written algorithms. Decide whether your representation permits them.
  • Vertex identity. Using mutable objects as vertex keys in a dict has the same hazard described under hash tables; integer or string IDs are safer.

Recall​

References​

  • Cormen, Leiserson, Rivest & Stein, Introduction to Algorithms, §22.1 — graph representations and their trade-offs.
  • Sedgewick & Wayne, Algorithms, 4th ed., Ch. 4 — "Graphs", covering undirected, directed, weighted and shortest-path graphs in turn.

Books & Videos​