SEN2212 · Week 10 · Analyzing and implementing graph structure using Java.

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.

graphvertexedgedirected graphundirected graphdegreeadjacency matrixadjacency listbreadth-first searchdepth-first searchconnected componentDAGtopological sortin-degreeKahn's algorithm

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:

  1. Representation: “Write the adjacency list/matrix of this graph”, “how much memory?”
  2. Traversal order: “Do BFS/DFS from 0 and write the visit order.” The rule is usually “visit neighbors in ascending order”.
  3. Topological sort: a valid course order in a prerequisite graph.
  4. Writing a method: BFS, DFS or counting components.

Terms

TermMeaning
vertex (node)an element of the graph, V of them
edgea connection between two vertices, E of them
undirectedthe edge u-v can be crossed in both directions
directed (digraph)the edge u→v only from u to v
weightedeach edge has a cost
degreethe number of edges touching the vertex; in a directed graph, in/out-degree
patha sequence of vertices connected by consecutive edges
cyclea path that ends at the vertex where it started
connectedthere is a path between every pair of vertices
DAGdirected 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 V(V−1)/2V(V-1)/2 edges (for directed, V(V−1)V(V-1)). 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 O(1)O(1). The disadvantage: V2V^2 memory regardless of the number of edges. For a road network with 1 million vertices, there would be 101210^{12} 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 O(V+E)O(V + E).

OperationMatrixList
MemoryO(V2)O(V^2)O(V+E)O(V + E)
Is there an edge (u, v)O(1)O(1)O(deg⁡u)O(\deg u)
All neighbors of uO(V)O(V)O(deg⁡u)O(\deg u)
BFS / DFSO(V2)O(V^2)O(V+E)O(V + E)

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

BFSDFS
Structurequeuestack / recursion
Traversallevel by leveldeep, then back
Unweighted shortest pathyesno
Typical useshortest path, levelsfinding cycles, topological order, components
Time (list)O(V+E)O(V + E)O(V+E)O(V + E)

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)

  1. Compute the in-degree of every vertex.
  2. Put the vertices with degree 0 in the queue.
  3. 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 O(V+E)O(V + E).

Common mistakes

Wrong ideaThe truth
Write an undirected edge into one listInto two lists: adj[u].add(v) and adj[v].add(u)
In BFS mark the vertex when removing itMark when adding; otherwise it is added again
DFS with a stack gives the same order as recursiveIf you push in ascending order the largest neighbor comes out first
DFS finds the shortest pathThe unweighted shortest path is with BFS
One BFS traverses the whole graphOnly the start’s component; one start per component
Topological order = DFS finishing orderThe reverse of the finishing order
The topological order is uniqueThere are usually many valid orders
Every directed graph can be topologically sortedOnly a DAG; if Kahn’s output is incomplete there is a cycle
BFS with a matrix is also O(V+E)O(V + E)O(V2)O(V^2); every row is scanned from start to end

Test yourself

Exam-style questions. Solve them on paper first, then reveal the answer.

Question 1easy

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
18; 45

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: (102)=10⋅9/2=45\binom{10}{2} = 10 \cdot 9 / 2 = 45. If it were directed it would be 90.

Question 2easy

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
6: [3, 4, 5]; 3: [0, 1, 6]

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.

Question 3medium

Run BFS on graph G starting at vertex 0 (neighbors visited in ascending order). Give the visit order.

Show answer
0 3 5 1 6 2 4 7

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.

Question 4medium

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
[0, 2, 2, 1, 3, 1, 2, 3]

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.

Question 5medium

Run recursive DFS on graph G starting at vertex 0 (neighbors in ascending order). Give the visit order.

Show answer
0 3 1 4 6 5 2 7

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.

Question 6hard

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
0 5 6 4 1 3 2 7

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.

Question 7medium

Run BFS on graph G starting at vertex 4. Give the visit order.

Show answer
4 1 6 3 5 0 2 7

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.

Question 8easy

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
Matrix: O(V²), O(1), O(V). Lists: O(V+E), O(deg u), O(deg u)

The matrix allocates one cell for each pair: V2V^2 memory, the edge check is a single index, but finding the neighbors scans the whole row. The list stores only the edges that exist: O(V+E)O(V+E). For sparse graphs (E much smaller than V2V^2) the list is chosen; BFS/DFS takes O(V+E)O(V+E) with a list and O(V2)O(V^2) with a matrix.

Question 9medium

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
4

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.

Question 10medium

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
4 6 2 0 5 1 3

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.

Question 11hard

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
finish 1 3 0 2 4 5 6; topo 6 5 4 2 0 3 1

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.

Question 12medium

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
Start a DFS/BFS from every unvisited vertex; count the starts

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: O(V+E)O(V + E).

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).

Review card

G = (V, E). Undirected: sum of degrees = 2E. Complete graph: V(V-1)/2 edges (directed V(V-1)). Matrix: V² memory, is there an edge O(1), neighbors O(V). For dense graphs. List: V+E memory, is there an edge O(deg), neighbors O(deg). For sparse graphs (most questions). An undirected edge is written into TWO lists. BFS = QUEUE. Goes level by level, gives the unweighted shortest path (number of edges). dist is written when first seen. DFS = recursion or a STACK. Goes to the end of one path, then backtracks. In stack DFS, if you push in ascending order the largest neighbor comes out first; to get the recursive order push in descending order. Mark WHEN ADDING to the queue (BFS), otherwise the same vertex is added twice. BFS and DFS are O(V+E) with a list, O(V²) with a matrix. Connected components: a new BFS/DFS from every unvisited vertex, number of starts = number of components. Topological order only in a DAG. Kahn: put in-degree 0 vertices in the queue, when dequeued decrease the degrees of the neighbors. If Kahn's output has fewer than V vertices the graph has a cycle. Topo with DFS: the REVERSE of the finishing (postorder) order. A topological order is usually not unique; the question's rule (queue or smallest label) decides.