Data structures & algorithms · Module 29
Graphs: representation, BFS and DFS
A graph is a list of who is next to whom. This page builds one from an edge list, then runs BFS and DFS from node 0 on the same six-node graph — same ten lines of code, one container swapped, two different visit orders.
Swap the queue for a stack yourself →01 The idea
Nodes, edges, and a frontier of things to look at next
A graph is a set of things plus the connections between them. The things are nodes, also called vertices. The connections are edges. Cities and roads, users and friendships, pages and links, board positions and legal moves — every one of these is a graph, and once you see the graph you can point the same two traversals at all of them.
You already know a special case. A tree is a graph with two restrictions: it is connected, and it has no cycles, so there is exactly one path between any two nodes. A general graph drops both. Two nodes can be joined by several paths, and a path can loop back to where it started. That is why graph traversal needs a visited set while walking a rooted tree does not: a tree's child pointers only ever point down, so you can never arrive back where you have been. Graph edges run both ways, and without a visited set even a single edge sends you back and forth forever.
02 Hand trace
From an edge list to a BFS order
Here is the graph the whole lesson uses. Seven edges, written the way input actually arrives: 0-1 1-2 2-3 3-4 4-5 5-0 0-3. Six nodes 0 to 5 sit in a ring, and one extra edge cuts across from 0 to 3. It is undirected, so every edge goes into the list twice, once at each end. If you added it only once you would have built a directed graph, where an edge points one way and 3 being a neighbour of 0 says nothing about 0 being a neighbour of 3.
| Node | adj[node] — the list | Row of the matrix |
|---|---|---|
| 0 | 1, 3, 5 | 0 1 0 1 0 1 |
| 1 | 0, 2 | 1 0 1 0 0 0 |
| 2 | 1, 3 | 0 1 0 1 0 0 |
| 3 | 0, 2, 4 | 1 0 1 0 1 0 |
| 4 | 3, 5 | 0 0 0 1 0 1 |
| 5 | 0, 4 | 1 0 0 0 1 0 |
Both columns hold the same graph. The list stores 14 numbers — two per edge. The matrix stores 36, and 22 of them are zeros standing for edges that do not exist. On six nodes that gap is small. On a social network with a million users and thirty friends each, the list holds 60 million entries and the matrix would need a million squared. That is the whole argument, and it is why the list is the default.
Now run BFS from node 0. Each row below is one node coming out of the queue. The boxes are the queue contents after that node was taken and its neighbours added, left box first — and the rose box is the front, the node that comes out next. A dot means the slot is empty.
Read the queue column, not the order column. Node 0 puts all three of its neighbours in at once, so 1, 3 and 5 all come out before anything two edges away is even discovered. That is what makes BFS level order: everything at distance 1 is sitting in the queue before the first node at distance 2 arrives. Nodes 2 and 4 are the only two at distance 2, and they are the last two out. Swap the queue for a stack and the newest node comes out first instead — the run stops fanning out and starts walking.
03 Code
One loop, two containers
The trace you just followed is ten lines of code. Here it is twice. Read the two blocks against each other: apart from the name on line 1 and the two comments, they differ on line 3 and line 5, in one word each, and nowhere else. BFS and DFS are not two algorithms you memorise separately. They are one algorithm with two choices of container.
BFS · frontier is a queue
function bfs(adj, start): seen = {start}; order = [] frontier = Queue([start]) # container while frontier is not empty: node = frontier.takeFront() # oldest out order.append(node) for nb in adj[node]: if nb not in seen: seen.add(nb); frontier.push(nb) return order
DFS · frontier is a stack
function dfs(adj, start): seen = {start}; order = [] frontier = Stack([start]) # container while frontier is not empty: node = frontier.takeTop() # newest out order.append(node) for nb in adj[node]: if nb not in seen: seen.add(nb); frontier.push(nb) return order
Only the container changes. Line 3 builds a Queue in one and a Stack in the other, and line 5 takes from the front in one and the top in the other. On this graph that gives BFS 0 1 3 5 2 4 and DFS 0 5 4 3 2 1. Same graph, same start, same seven edges, same number of steps — different order, because a queue returns the oldest thing you put in and a stack returns the newest.
Mark visited on line 9, when you push — not when you pop. Node 2 is a neighbour of both 1 and 3. Marking at push time means 3 finds it already seen and it enters the queue exactly once. Move the marking to pop time and you must also skip a node that comes out already done — the visit order still comes out right, and every edge is still looked at once, so the time stays O(V + E). What breaks is memory: a node can now be pushed once for every neighbour that finds it, so the frontier holds up to O(E) entries instead of O(V). On six nodes joined to each other that is 16 pushes instead of 6.
A stack hands the neighbours back in reverse. adj[0] is [1, 3, 5], so BFS reaches 1 first. The stack version pushes 1, then 3, then 5, and 5 is now on top — so from node 0 it goes to 5 first and then walks 0 → 5 → 4 → 3 → 2 → 1, right round the ring. Recursive DFS taking the list in order would reach 1 first instead and give 0 1 2 3 4 5.
The stack version is not just the recursion with the neighbours flipped. Marking at push time claims a node the moment anyone finds it, so the walk can be cut off from below: start the console at node 3 and the stack gives 3 4 5 2 1 0, but no recursive DFS can produce that — standing on 5 with 0 not yet in the order, recursion is obliged to go deeper into 0, and the stack version is not, because 0 was already marked back at 3. Both traversals are correct: each reaches every node exactly once and each walks depth-ward before it fans out. They are simply not the same order, and flipping the neighbour list does not fix it in general. If you specifically need the recursive order from a loop, mark on pop instead, skip nodes already done, and push each neighbour list reversed — that one reproduces it exactly.
Recursive DFS is the same idea with the call stack doing the work. dfs(node): seen.add(node); order.append(node); for nb in adj[node]: if nb not in seen: dfs(nb). Nothing has disappeared — the frontier is now the pile of pending call frames, so the depth of the graph is your memory bill and a chain of 100,000 nodes will overflow it.
05 Cheat sheet
The numbers you get asked for
| What you ask of the graph | Adjacency list | Adjacency matrix |
|---|---|---|
| Memory for V nodes and E edges | O(V + E) | O(V²) |
| Are u and v joined? | O(deg u) | O(1) |
| List u's neighbours | O(deg u) | O(V) |
| One full BFS or DFS | O(V + E) | O(V²) |
| Add an edge | O(1) | O(1) |
| This lesson's graph, V = 6 and E = 7 | 14 numbers | 36 numbers |
06 Where & why
Picking the container on purpose
BFS and DFS cost the same and reach the same set of nodes. They are still not interchangeable, because of what they hand you on the way. Pick by the information you need, not by which one you remember first.
Reach for BFS when…
Use something else when…
| Situation | Better choice | Why |
|---|---|---|
| You need the order calls enter and leave — directed-cycle detection, or a topological order read off finish times | Recursive DFS | Neither the queue nor the push-marking stack produces entry and exit times; recursion gives them for free. There are other routes to the same answers — a topological order also falls out of BFS by repeatedly taking a node of indegree zero — but they are different algorithms, not this loop. |
| You ask whether two nodes are connected over and over, with edges arriving as you go | Union-find | It answers repeated queries in near-constant time with no traversal at all, and merges components as the edges arrive. For one question on a fixed graph, a single traversal is simpler and just as fast. |
| Edges carry different weights | Dijkstra | BFS counts edges, not cost, so it will hand you a two-edge path that is more expensive than a three-edge one. |
| The graph is dense — E close to V² | An adjacency matrix | The list stops saving memory, and the matrix answers “are u and v joined?” in O(1) instead of O(deg u). |
07 Interview questions
Say these out loud
Question 7 is the one that separates people who have written BFS from people who have only read it.
What is a graph, and how is a tree different?
Adjacency list or adjacency matrix — which do you use?
How do you build an adjacency list from an edge list?
What is the actual difference between BFS and DFS?
Time and space complexity of BFS and DFS?
Why does BFS give the shortest path on an unweighted graph, and DFS not?
Where exactly do you mark a node visited?
What happens if you drop the visited set entirely?
How do you count connected components?
Your iterative DFS gives a different order to your recursive DFS — is one wrong?
Is “number of islands” really a graph problem?
When would you use neither BFS nor DFS?
08 Practice problems
Build it, then traverse it
For each one, decide what the nodes are and what the edges are before you write a line, then name the container. Every problem here is solvable with the ten lines from section 03 and a little bookkeeping — nothing needs an algorithm this page has not shown you.