Graphs: representation, BFS, DFS and topological sort
Storing a graph with an adjacency matrix and list, traversing with BFS and DFS, counting connected components and ordering a DAG topologically; the rules behind the exam's 'write the visit order' questions.
In trees every node had a single parent. A graph removes this restriction: there can be a connection between any two vertices, there can be cycles, there can be no connection at all. A road map, a social network, course prerequisites and internet links are all graphs. Next week we will run shortest-path algorithms on them; this week we lay the foundation: how do we store a graph and how do we traverse it.
The questions that will come from this week in the exam:
- Representation: “Write the adjacency list/matrix of this graph”, “how much memory?”
- Traversal order: “Do BFS/DFS from 0 and write the visit order.” The rule is usually “visit neighbors in ascending order”.
- Topological sort: a valid course order in a prerequisite graph.
- Writing a method: BFS, DFS or counting components.
Terms
| Term | Meaning |
|---|---|
| vertex (node) | an element of the graph, V of them |
| edge | a connection between two vertices, E of them |
| undirected | the edge u-v can be crossed in both directions |
| directed (digraph) | the edge u→v only from u to v |
| weighted | each edge has a cost |
| degree | the number of edges touching the vertex; in a directed graph, in/out-degree |
| path | a sequence of vertices connected by consecutive edges |
| cycle | a path that ends at the vertex where it started |
| connected | there is a path between every pair of vertices |
| DAG | directed acyclic graph |
In an undirected graph each edge contributes to the degree of two vertices, so the sum of degrees = 2E. A graph with V vertices has at most edges (for directed, ). If E is near this limit the graph is dense, and if it is on the order of V it is sparse.
The example graph G
All the traversal examples on this page use this graph:
1 --- 3 --- 0
| | |
4 --- 6 --- 5 --- 2 --- 7
Edges: 0-3, 0-5, 1-3, 1-4, 2-5, 2-7, 3-6, 4-6, 5-6. In total 9 edges, the sum of degrees is 18.
Representation 1: the adjacency matrix
A V×V boolean or int array: if a[u][v] = 1 there is an edge. In an undirected graph the matrix is symmetric. A small example, edges 0-1, 0-2, 1-2, 2-3:
0 1 2 3
0 [ 0, 1, 1, 0 ]
1 [ 1, 0, 1, 0 ]
2 [ 1, 1, 0, 1 ]
3 [ 0, 0, 1, 0 ]
In a weighted graph the weight is written instead of 1, and infinity (or 0) is put if there is no edge. The advantage: the question “are u and v connected?” is . The disadvantage: memory regardless of the number of edges. For a road network with 1 million vertices, there would be cells.
Representation 2: the adjacency list
For each vertex, a list of its neighbors. In Java the most practical form is List<List<Integer>>:
int n = 8;
List<List<Integer>> adj = new ArrayList<>();
for (int i = 0; i < n; i++) adj.add(new ArrayList<>());
int[][] edges = {{0,3},{0,5},{3,1},{3,6},{5,6},{5,2},{1,4},{6,4},{2,7}};
for (int[] e : edges) {
adj.get(e[0]).add(e[1]); // undirected: add in both directions
adj.get(e[1]).add(e[0]);
}
for (List<Integer> l : adj) Collections.sort(l);
System.out.println(adj);
[[3, 5], [3, 4], [5, 7], [0, 1, 6], [1, 6], [0, 2, 6], [3, 4, 5], [2]]
The total length of the lists is 2E for undirected and E for directed. Memory is .
| Operation | Matrix | List |
|---|---|---|
| Memory | ||
| Is there an edge (u, v) | ||
| All neighbors of u | ||
| BFS / DFS |
Since real graphs are almost always sparse, the default choice is the list.
Breadth-first search (BFS)
BFS spreads from the start level by level: first those 1 edge away, then 2, then 3. The tool is a queue.
static void bfs(List<List<Integer>> adj, int s) {
int[] dist = new int[adj.size()];
Arrays.fill(dist, -1); // -1 = not seen
Deque<Integer> q = new ArrayDeque<>();
dist[s] = 0;
q.add(s);
while (!q.isEmpty()) {
int v = q.poll();
System.out.print(v + " ");
for (int w : adj.get(v))
if (dist[w] == -1) { // seen for the first time
dist[w] = dist[v] + 1;
q.add(w);
}
}
}
The trace from 0 on G:
dequeued added queue
0 3, 5 [3, 5]
3 1, 6 [5, 1, 6]
5 2 [1, 6, 2]
1 4 [6, 2, 4]
6 - [2, 4]
2 7 [4, 7]
4 - [7]
7 - []
order: 0 3 5 1 6 2 4 7
dist: 0:0 1:2 2:2 3:1 4:3 5:1 6:2 7:3
The superpower of BFS: in an unweighted graph the dist array is exactly the number of edges of the shortest path. A vertex enters the queue for the first time with its shortest distance, because the queue processes distances in order. If you keep a parent array you can also reconstruct the path itself.
PITFALL · when do you mark? Mark the vertex as seen when adding it to the queue. If you mark when removing, the same vertex enters the queue more than once: 6 would be added from both 3 and 5. The order and the complexity break.
Depth-first search (DFS)
DFS goes as far as it can along one path, and when it hits a dead end it goes back (backtracking). Its recursive form is very similar to the preorder traversal of a tree; the only difference is the visited array, because a graph has cycles.
static void dfs(List<List<Integer>> adj, int v, boolean[] visited) {
visited[v] = true;
System.out.print(v + " ");
for (int w : adj.get(v))
if (!visited[w]) dfs(adj, w, visited);
}
From 0 on G:
dfs(0) -> 3
dfs(3) -> 0 seen, 1
dfs(1) -> 3 seen, 4
dfs(4) -> 1 seen, 6
dfs(6) -> 3, 4 seen, 5
dfs(5) -> 0 seen, 2
dfs(2) -> 5 seen, 7
dfs(7) -> go back
order: 0 3 1 4 6 5 2 7
DFS with a stack
Instead of recursion, an explicit stack can be used: pop a vertex, skip it if it is visited, otherwise visit it and push its unvisited neighbors. Here the order changes:
push neighbors in ASCENDING order: 0 5 6 4 1 3 2 7
push neighbors in DESCENDING order: 0 3 1 4 6 5 2 7
Since the stack works on last-in first-out logic, if you push in ascending order the largest neighbor comes out first. If you want the same order as recursive DFS, push the neighbors in reverse order. If the exam says “using a stack”, be sure to account for this difference.
BFS and DFS compared
| BFS | DFS | |
|---|---|---|
| Structure | queue | stack / recursion |
| Traversal | level by level | deep, then back |
| Unweighted shortest path | yes | no |
| Typical use | shortest path, levels | finding cycles, topological order, components |
| Time (list) |
Connected components
If the graph is not connected, a single BFS/DFS cannot reach every vertex. The solution: go through the vertices in order, and start a new traversal from every unvisited one. Each start means one component.
Vertices 0..8, edges 0-1, 1-2, 3-4, 5-6, 6-7, 5-7:
0 - 1 - 2 3 - 4 5 - 6 8
\ /
7
component labels: [0,0,0,1,1,2,2,2,3]
number of components: 4
The vertex 8, which has no edges, is also a component on its own. The whole code is in the question bank.
Topological sort
An ordering of vertices in a directed graph in which, for every edge u→v, u comes before v. The classic example is course prerequisites: if SEN2211 → SEN2212, then 2211 is taken first. It is possible only in a DAG; if there is a cycle (A is a prerequisite of B and B of A) there is no valid order.
An example DAG, edges 4→2, 4→0, 6→0, 6→5, 2→1, 0→1, 0→3, 5→3:
4 ---> 2 ---> 1
\ ^
v |
0 ---------+
^ \
/ v
6 3
\ ^
v |
5 -+
Kahn’s algorithm (with in-degrees)
- Compute the in-degree of every vertex.
- Put the vertices with degree 0 in the queue.
- Dequeue, write to the output. Decrease the degree of its neighbors by 1; add the one that drops to 0 to the queue.
in-degree: 0:2 1:2 2:1 3:2 4:0 5:1 6:0
queue [4, 6]
dequeue 4 -> 2 freed [6, 2]
dequeue 6 -> 0, 5 freed [2, 0, 5]
dequeue 2 -> (1 is still 1) [0, 5]
dequeue 0 -> 1 freed [5, 1]
dequeue 5 -> 3 freed [1, 3]
dequeue 1, dequeue 3
order: 4 6 2 0 5 1 3
If you use a min-heap instead of a queue and pick the smallest label each time, the order is 4 2 6 0 1 5 3. Both are valid: a topological order is usually not unique, and the rule in the question decides.
Cycle detection comes for free: if there are fewer than V vertices in the output when Kahn finishes, the graph has a cycle, because the degrees of the vertices in the cycle never drop to 0. In the graph with edges 0→1, 1→2, 2→0, 2→3, no vertex has in-degree 0; the output has 0 vertices.
Topological order with DFS
In DFS a vertex finishes after all its descendants have finished. So for an edge u→v, v finishes before u. If you reverse the finishing order, you get the topological order.
dfs(0): 1 finishes, 3 finishes, 0 finishes
dfs(2): 2 finishes dfs(4): 4 finishes
dfs(5): 5 finishes dfs(6): 6 finishes
finish: 1 3 0 2 4 5 6
reversed: 6 5 4 2 0 3 1
Check: 6→0 (6 first), 4→2, 2→1, 0→3, 5→3: all in the right direction. Both algorithms are .
Common mistakes
| Wrong idea | The truth |
|---|---|
| Write an undirected edge into one list | Into two lists: adj[u].add(v) and adj[v].add(u) |
| In BFS mark the vertex when removing it | Mark when adding; otherwise it is added again |
| DFS with a stack gives the same order as recursive | If you push in ascending order the largest neighbor comes out first |
| DFS finds the shortest path | The unweighted shortest path is with BFS |
| One BFS traverses the whole graph | Only the start’s component; one start per component |
| Topological order = DFS finishing order | The reverse of the finishing order |
| The topological order is unique | There are usually many valid orders |
| Every directed graph can be topologically sorted | Only a DAG; if Kahn’s output is incomplete there is a cycle |
| BFS with a matrix is also | ; every row is scanned from start to end |
Test yourself
Exam-style questions. Solve them on paper first, then reveal the answer.
An undirected graph has 9 edges. What is the sum of the degrees of all vertices? How many edges does a complete undirected graph on 10 vertices have?
Show answer
Each edge increases the degree of its two endpoints by one: sum of degrees = 2E = 18 (handshake lemma). In a complete graph every pair is connected: . If it were directed it would be 90.
For graph G (edges 0-3, 0-5, 1-3, 1-4, 2-5, 2-7, 3-6, 4-6, 5-6), give the adjacency list of vertex 6 and of vertex 3 (ascending order).
Show answer
In an undirected graph each edge is written into two lists. The edges containing 6 are 3-6, 4-6, 5-6 → [3, 4, 5]. Those containing 3 are 0-3, 1-3, 3-6 → [0, 1, 6]. Degree(6) = degree(3) = 3.
Run BFS on graph G starting at vertex 0 (neighbors visited in ascending order). Give the visit order.
Show answer
Queue: [0]. 0 is dequeued, its neighbors 3, 5 are added → [3, 5].
3 is dequeued: 0 is seen, 1 and 6 are added → [5, 1, 6].
5 is dequeued: 0 and 6 are seen, 2 is added → [1, 6, 2].
1 is dequeued: 4 is added → [6, 2, 4]. 6 is dequeued: all seen. 2 is dequeued: 7 is added. 4 and 7 are dequeued.
Order: 0 3 5 1 6 2 4 7.
Using the BFS from vertex 0 on graph G, give the shortest-path distance (number of edges) from 0 to every vertex 0..7.
Show answer
When BFS sees a vertex for the first time it writes dist[w] = dist[v] + 1. 3 and 5 are direct from 0: 1. 1 and 6 (via 3) and 2 (via 5): 2. 4 (via 1) and 7 (via 2): 3. In an unweighted graph BFS gives the shortest path.
Run recursive DFS on graph G starting at vertex 0 (neighbors in ascending order). Give the visit order.
Show answer
0 → the first unvisited neighbor 3 → the first unvisited neighbor of 3 is 1 → the neighbor of 1 is 4 → the neighbor of 4 is 6 (1 is seen) → the neighbors of 6 are 3 (seen), 4 (seen), 5 → the neighbors of 5 are 0, 2 → 2 → 7.
The return starts from 7, and all the remaining neighbors are seen. Order: 0 3 1 4 6 5 2 7.
DFS on graph G is now implemented with an explicit stack: pop a vertex, skip it if visited, otherwise visit it and push all unvisited neighbors in ascending order. Starting at 0, what is the visit order?
Show answer
Since the stack is LIFO, the neighbor pushed last (the largest) comes out first.
[0] → visit 0, push 3, 5 → [3, 5]
pop 5 → visit, push 2, 6 → [3, 2, 6]
pop 6 → visit, push 3, 4 → [3, 2, 3, 4]
pop 4 → visit, push 1 → [3, 2, 3, 1]
pop 1 → visit, push 3 → [3, 2, 3, 3]
pop 3 → visit; the next two 3s are skipped → [2]
pop 2 → visit, push 7 → pop 7.
If you want the recursive order (0 3 1 4 6 5 2 7), you must push the neighbors in descending order.
Run BFS on graph G starting at vertex 4. Give the visit order.
Show answer
The neighbors of 4 are 1, 6 → [1, 6]. 1 is dequeued: 3 → [6, 3]. 6 is dequeued: 5 (3 is already in the queue) → [3, 5]. 3 is dequeued: 0 → [5, 0]. 5 is dequeued: 2 → [0, 2]. 0 is dequeued: nothing new. 2 is dequeued: 7. Order 4 1 6 3 5 0 2 7.
Compare an adjacency matrix and adjacency lists for a graph with V vertices and E edges: memory, checking whether edge (u, v) exists, and iterating over all neighbors of u.
Show answer
The matrix allocates one cell for each pair: memory, the edge check is a single index, but finding the neighbors scans the whole row. The list stores only the edges that exist: . For sparse graphs (E much smaller than ) the list is chosen; BFS/DFS takes with a list and with a matrix.
An undirected graph has vertices 0..8 and edges 0-1, 1-2, 3-4, 5-6, 6-7, 5-7. How many connected components does it have?
Show answer
The components: {0, 1, 2}, {3, 4}, {5, 6, 7} and {8}, which has no edges. A vertex on its own is also a component. The algorithm: start a BFS/DFS from every unvisited vertex, and each start is one component.
A DAG has edges 4→2, 4→0, 6→0, 6→5, 2→1, 0→1, 0→3, 5→3. Run Kahn's algorithm with a FIFO queue (initial zero in-degree vertices enqueued in ascending order; newly freed vertices enqueued in adjacency order). Give the topological order.
Show answer
In-degrees: 0:2, 1:2, 2:1, 3:2, 4:0, 5:1, 6:0. Queue [4, 6].
4 is dequeued: the degree of 2 becomes 0 → add; the degree of 0 becomes 1 → [6, 2]
6 is dequeued: 0 → 0, 5 → 0 → [2, 0, 5]
2 is dequeued: the degree of 1 becomes 1 → [0, 5]
0 is dequeued: 1 → 0, 3 → 1 → [5, 1]
5 is dequeued: 3 → 0 → [1, 3]. Then 1, 3.
If you had used a min-heap instead of a queue (the smallest label each time), the order would be 4 2 6 0 1 5 3.
For the same DAG, run DFS from vertices 0, 1, ..., 6 in order (skipping visited ones, neighbors in ascending order). Give the finishing order and the topological order it produces.
Show answer
DFS(0): go to 1, 1 has no outgoing edges → 1 finishes. Go to 3 → 3 finishes. 0 finishes.
DFS(2): 1 is seen → 2 finishes. DFS(4): 0 and 2 are seen → 4 finishes. DFS(5): 3 is seen → 5 finishes. DFS(6) → 6 finishes.
The finishing order is 1 3 0 2 4 5 6. The topological order is the REVERSE of it: 6 5 4 2 0 3 1. For every edge u→v, u comes before v; check it.
Write a method int countComponents(List<List<Integer>> adj) that returns the number of connected components of an undirected graph given as adjacency lists.
Show answer
Every new start means a new component.
static int countComponents(List<List<Integer>> adj) {
int n = adj.size(), count = 0;
boolean[] visited = new boolean[n];
for (int v = 0; v < n; v++) {
if (!visited[v]) {
count++;
dfs(adj, v, visited);
}
}
return count;
}static void dfs(List<List<Integer>> adj, int v, boolean[] visited) {
visited[v] = true;
for (int w : adj.get(v))
if (!visited[w]) dfs(adj, w, visited);
}Each vertex is marked once and each list is scanned once: .
Verification: Adjacency lists, BFS/DFS orders (recursive and with a stack), BFS distances, the component count, Kahn (queue and min-PQ) and DFS finishing-order topological orders were run with Java (verify/SEN2212/W10.java, java W10.java).