Graph
A data structure of nodes and edges, explored with BFS and DFS.
#How does it work?
A graph consists of nodes (vertices) and the edges that connect them. Road maps, social networks, computer networks and links between web pages are all modelled as graphs.
Edges can be directed (a one-way street, following someone) or undirected (a two-way road, friendship), and can carry weights (distance, cost). This lesson uses an undirected, unweighted graph.
Graphs are usually stored as adjacency lists: each node keeps a list of its neighbours. The alternative is an adjacency matrix: an n×n table with a 1 where an edge between i and j exists. The two basic ways to explore a graph are breadth-first search (BFS) and depth-first search (DFS).
#Try it yourself
Adjacency list
- 1: 2, 3
- 2: 1, 4, 5
- 3: 1, 5, 6
- 4: 2, 7
- 5: 2, 3, 7
- 6: 3, 7
- 7: 4, 5, 6
Pick a start node and compare BFS with DFS: watch the order they visit nodes in and the blue traversal tree. Add or remove edges to change the graph.
#Basic operations
- BFS(start)
- Spreads out from the start layer by layer: first all neighbours, then their neighbours. Uses a queue. Finds shortest paths (fewest edges) in unweighted graphs.
- DFS(start)
- Goes as deep as possible along one path, then backtracks at dead ends. Uses recursion or a stack. The basis of cycle detection, connected components and topological sorting.
- addEdge(u, v)
- In an undirected graph, adds v to u's adjacency list and u to v's.
- removeEdge(u, v)
- Removes the edge from both adjacency lists.
#Example code
The same algorithm in 8 languages. Every program is compiled, run and checked to print the same output.
from collections import deque
# Adjacency list: graph[u] holds u's neighbours.
graph: dict[int, list[int]] = {node: [] for node in range(1, 8)}
for u, v in [(1, 2), (1, 3), (2, 4), (2, 5), (3, 5), (3, 6), (4, 7), (5, 7), (6, 7)]:
graph[u].append(v) # undirected: add both directions
graph[v].append(u)
def bfs(start: int) -> list[int]:
visited = {start}
queue = deque([start])
order = []
while queue:
u = queue.popleft()
order.append(u)
for v in graph[u]:
if v not in visited: # mark when queued so each node is queued once
visited.add(v)
queue.append(v)
return order
def dfs(u: int, visited: set[int], order: list[int]) -> list[int]:
visited.add(u)
order.append(u)
for v in graph[u]:
if v not in visited:
dfs(v, visited, order)
return order
print(*bfs(1)) # layer by layer
print(*dfs(1, set(), [])) # as deep as possible, then backtrack#Complexity
| Operation | Time |
|---|---|
| BFS / DFS | O(V + E) |
| Memory (adjacency list) | O(V + E) |
| Memory (adjacency matrix) | O(V²) |
| Edge exists? (list) | O(degree) |
| Edge exists? (matrix) | O(1) |
| Add edge (list) | O(1) |
V is the number of nodes and E the number of edges. BFS and DFS visit every node once and look at every edge (from both ends in an undirected graph): O(V + E). Adjacency lists use far less memory for sparse graphs (few edges); a matrix is preferred for dense graphs or when edge lookups are very frequent.
#When to use it?
Shortest routes in navigation (Dijkstra, A*), "people you may know" in social networks (second-degree neighbours via BFS), web crawlers, build order in compilers (topological sort), maze solving and network connectivity analysis all rely on graph algorithms.
#Practice
Log in to answer questions and save your progress.
Easy
Which helper data structure does BFS use?
- 1Queue
- 2Stack
- 3Hash table
- 4Heap
What is the complexity of BFS on a graph stored as adjacency lists?
- 1O(V + E)
- 2O(V²)
- 3O(E log V)
- 4O(V · E)
What does DFS do at a dead end?
- 1Backtracks
- 2Stops
- 3Restarts from the beginning
- 4Jumps to a random node
Medium
In a graph with edges 1-2, 1-3, 2-4, 3-4, in what order does BFS from 1 visit nodes (neighbours in ascending order)?
- 11, 2, 3, 4
- 21, 2, 4, 3
- 31, 3, 4, 2
- 41, 4, 2, 3
Hard
Which one guarantees the shortest path (fewest edges) between two nodes in an unweighted graph?
- 1BFS
- 2DFS
- 3Both
- 4Neither
#Up next
Lessons that build on this one: