Data Structures & Algorithms (DSA) Module 11 – Graphs: modelling, traversal and connectivity
Depth-first search, components and timestamps
How depth-first search goes deep before backing up, recursively or with a stack, counts connected components and records discovery and finish times.
What you will learn
- Implement DFS recursively and with an explicit stack
- Count connected components
- Use discovery and finish times to classify edges
Before you start
What you’ll learn · Before you start
What you will learn
- Implement DFS recursively and with an explicit stack
- Count connected components
- Use discovery and finish times to classify edges
Before you start
On this page
Depth-first search (DFS) follows one path as far as it can and backs up only when the current vertex has no unvisited neighbour left. Written recursively it is a few lines: mark the vertex, then search from each unmarked neighbour. Like breadth-first search it visits every vertex and edge once, in O(V + E), but in a different order, and that order carries information. Starting a new search from every vertex no earlier search reached counts the connected components of an undirected graph. A clock that ticks when a vertex is discovered and again when it is finished gives every vertex two timestamps, and those times sort every edge into one of four kinds, one of which reveals cycles. On deep graphs the recursion can exceed the language’s call stack, so the explicit-stack version matters as much as the recursive one.
Going deep, then backing up
The trace below runs a recursive DFS from vertex 0 of a small directed graph. Each line is indented by the depth of the recursion, and the clock ticks once at each discovery and once at each finish:
# Recursive depth-first search on a small directed graph, with a clock that ticks once when a vertex is discovered and
# once when it is finished. Each line is indented by the depth of the recursion, so the printout is the DFS tree.
adj = {0: [1, 4, 3], 1: [2], 2: [0, 3], 3: [], 4: [3, 5], 5: [6], 6: [4]}
clock = 0
discovered = {}
finished = {}
def dfs(u, depth):
global clock
clock += 1
discovered[u] = clock
print(f"{' ' * depth}discover {u} at {clock}")
for v in adj[u]:
if v not in discovered:
dfs(v, depth + 1)
else:
state = "still open" if v not in finished else "already finished"
print(f"{' ' * (depth + 1)}{u} -> {v}: {v} is {state}")
clock += 1
finished[u] = clock
print(f"{' ' * depth}finish {u} at {clock}")
for s in adj: # start again from every vertex not yet discovered (here one start is enough)
if s not in discovered:
dfs(s, 0)
print()
print("vertex discovered finished")
for u in sorted(adj):
print(f"{u:>6} {discovered[u]:>10} {finished[u]:>8}") Output
discover 0 at 1
discover 1 at 2
discover 2 at 3
2 -> 0: 0 is still open
discover 3 at 4
finish 3 at 5
finish 2 at 6
finish 1 at 7
discover 4 at 8
4 -> 3: 3 is already finished
discover 5 at 9
discover 6 at 10
6 -> 4: 4 is still open
finish 6 at 11
finish 5 at 12
finish 4 at 13
0 -> 3: 3 is already finished
finish 0 at 14
vertex discovered finished
0 1 14
1 2 7
2 3 6
3 4 5
4 8 13
5 9 12
6 10 11
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 dfs.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
DFS from vertex 0: discovery / finish times, tree edges solid, the other edges dashed
Text description of the diagram
Each vertex is labelled "vertex · discovery time / finish time".
- Tree edges (solid): 0 to 1, 1 to 2, 2 to 3, then 0 to 4, 4 to 5 and 5 to 6. Vertex 0 is discovered at 1 and finished at 14; 1 at 2 and 7; 2 at 3 and 6; 3 at 4 and 5; 4 at 8 and 13; 5 at 9 and 12; 6 at 10 and 11.
- Dashed edges: 2 to 0 and 6 to 4 are back edges, each pointing to a vertex that was still open; 0 to 3 is a forward edge, to a descendant that was already finished; 4 to 3 is a cross edge, to a vertex finished before 4 was discovered.
The intervals nest along each tree branch: 3's interval, 4 to 5, lies inside 2's, 3 to 6, which lies inside 1's, 2 to 7, inside 0's, 1 to 14.
The search went 0, 1, 2, 3 before it backed up, then took 0’s next neighbour, 4, and went down 4, 5, 6. Depth-first search keeps the current path on the call stack, and that path is what it backs up along. Every vertex gets its two timestamps, and between the two the search explores everything reachable from it that is still unvisited. The edges it follows to undiscovered vertices form the DFS tree; with several starts they form a forest. DFS sorts every edge into one of four kinds, named by what it finds at the far end of the edge:
- A tree edge leads to a vertex not yet discovered, and DFS follows it.
- A back edge leads to a vertex that is still open, an ancestor on the current path, as 2 → 0 does.
- A forward edge leads to a descendant that is already finished, as 0 → 3 does.
- A cross edge leads to a vertex finished before the current one was discovered, as 4 → 3 does.
A back edge closes a directed cycle with the tree path below its target, which is how DFS finds cycles in the lesson that follows.
Counting components
In an undirected graph, one search from a vertex marks exactly that vertex’s connected component. So loop over all vertices and start a new search from each one that is still unmarked: the number of starts is the number of components. On a grid the same idea is called flood fill. This map of land and water uses a list as an explicit stack instead of recursion:
# Connected components of a made-up map: # is land, . is water, and land cells that touch up, down, left or right
# belong to one island. Each search starts from a land cell no earlier search reached, and uses an explicit stack
# instead of recursion, so a very large island cannot exceed Python's recursion limit.
sea = [
"##....#####.........",
"##.....###.....##...",
"......##.......##...",
".#.................#",
".###....######.....#",
"..#.....#....#..####",
"........######..#..#",
"##..............####",
]
rows, cols = len(sea), len(sea[0])
label = [[None] * cols for _ in range(rows)]
sizes = []
for r0 in range(rows):
for c0 in range(cols):
if sea[r0][c0] != "#" or label[r0][c0] is not None:
continue
island = len(sizes)
label[r0][c0] = island
stack = [(r0, c0)]
size = 0
while stack:
r, c = stack.pop()
size += 1
for nr, nc in ((r - 1, c), (r + 1, c), (r, c - 1), (r, c + 1)):
if 0 <= nr < rows and 0 <= nc < cols and sea[nr][nc] == "#" and label[nr][nc] is None:
label[nr][nc] = island
stack.append((nr, nc))
sizes.append(size)
letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
for r in range(rows):
print("".join("." if label[r][c] is None else letters[label[r][c]] for c in range(cols)))
print()
print(f"{len(sizes)} islands, sizes", ", ".join(f"{letters[i]} {s}" for i, s in enumerate(sizes)))
print(f"{sum(sizes)} land cells in all; the largest island has {max(sizes)}") Output
AA....BBBBB......... AA.....BBB.....CC... ......BB.......CC... .D.................E .DDD....FFFFFF.....E ..D.....F....F..EEEE ........FFFFFF..E..E GG..............EEEE 7 islands, sizes A 4, B 10, C 4, D 5, E 12, F 14, G 2 51 land cells in all; the largest island has 14
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 islands.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Each letter is one component. Island F is a ring around a lake and island E has a hole, and neither confuses the search, because it only follows land cells that touch.
Recursion or an explicit stack
Each recursive call waits for the next one, so the recursion is as deep as the longest path the search walks down,
up to V. CPython stops at sys.getrecursionlimit(), 1,000 by default, so a recursive DFS fails on a path of a few
thousand vertices. A list used as a stack lives on the heap and avoids that. The simple version, which pops a vertex
and pushes all its unmarked neighbours, finds the same components but visits vertices in a different order from the
recursive one, so it gives different timestamps.
| Operation | Time | Extra space | Note |
|---|---|---|---|
| One search, or all of them across the components | O(V + E) | O(V) | each vertex marked once, each list read once |
| Recursion depth, worst case | - | O(V) | a path of V vertices |
| Discovery and finish times | O(V) | O(V) | two ticks per vertex |
GATE CS
GATE questions on graph traversals can ask which orders a DFS may produce, or for the discovery and finish times of a small graph. Ticking the clock once at each discovery and once at each finish, as the trace does, gives times from 1 to 2V, and the intervals of a vertex and its descendants nest.
Exercise and self-check
Count the connected components of graphs with up to 200,000 vertices, including one path through all of them.
Exercise · Medium · Python, JavaScript
Count the connected components of a large graph
An undirected graph has n vertices, numbered 0 to n - 1, and a list of edges (u, v) (JavaScript: two-element arrays). Write count_components(n, edges) (JavaScript: countComponents(n, edges)), which returns the number of connected components: isolated vertices count as components of their own, and loops and repeated edges change nothing.
n and the number of edges go up to 2 × 10⁵. The tests include small graphs, a graph with no edges, random graphs checked against a slow method, and a single path through all 200,000 vertices, whose depth-first search would be 200,000 calls deep if it were written with recursion: far beyond Python's default limit of 1,000.
Python · Starter code · components.py
def count_components(n, edges):
"""The number of connected components of an undirected graph on vertices 0..n-1."""
raise NotImplementedError("write count_components") The sample tests · test_components.py
import random
from components import count_components
def slow_count(n, edges):
"""merge labels along the edges until nothing changes"""
label = list(range(n))
changed = True
while changed:
changed = False
for u, v in edges:
low = min(label[u], label[v])
if label[u] != low or label[v] != low:
label[u] = label[v] = low
changed = True
return len(set(label))
def test_small_graphs():
"""the lesson's eight-vertex graph has three components; a triangle has one"""
assert count_components(8, [(0, 1), (0, 2), (1, 2), (2, 3), (3, 4), (5, 6)]) == 3
assert count_components(3, [(0, 1), (1, 2), (2, 0)]) == 1
def test_no_edges():
"""every vertex of a graph without edges is a component of its own"""
assert count_components(5, []) == 5
assert count_components(1, []) == 1
def test_loops_and_repeats():
"""a loop and a repeated edge change nothing"""
assert count_components(4, [(0, 0), (1, 2), (2, 1), (1, 2)]) == 3
def test_random_graphs():
"""on 300 random graphs, matches a slow method that merges labels"""
rng = random.Random(3)
for _ in range(300):
n = rng.randint(1, 30)
edges = [(rng.randrange(n), rng.randrange(n)) for _ in range(rng.randint(0, 40))]
assert count_components(n, edges) == slow_count(n, edges), (n, edges)
def test_long_path():
"""one path through 200,000 vertices, given in shuffled order: one component"""
n = 200_000
rng = random.Random(8)
order = list(range(n))
rng.shuffle(order)
edges = [(order[i], order[i + 1]) for i in range(n - 1)]
rng.shuffle(edges)
assert count_components(n, edges) == 1
assert count_components(n, edges[1:]) == 2 JavaScript · Starter code · components.mjs
/** The number of connected components of an undirected graph on vertices 0..n-1. */
export function countComponents(n, edges) {
throw new Error('write countComponents');
} The sample tests · components.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { countComponents } from './components.mjs';
function slowCount(n, edges) {
const label = Array.from({ length: n }, (_, i) => i);
let changed = true;
while (changed) {
changed = false;
for (const [u, v] of edges) {
const low = Math.min(label[u], label[v]);
if (label[u] !== low || label[v] !== low) {
label[u] = label[v] = low;
changed = true;
}
}
}
return new Set(label).size;
}
test("the lesson's eight-vertex graph has three components; a triangle has one", () => {
assert.equal(countComponents(8, [[0, 1], [0, 2], [1, 2], [2, 3], [3, 4], [5, 6]]), 3);
assert.equal(countComponents(3, [[0, 1], [1, 2], [2, 0]]), 1);
});
test('every vertex of a graph without edges is a component of its own', () => {
assert.equal(countComponents(5, []), 5);
assert.equal(countComponents(1, []), 1);
});
test('a loop and a repeated edge change nothing', () => {
assert.equal(countComponents(4, [[0, 0], [1, 2], [2, 1], [1, 2]]), 3);
});
test('on 300 random graphs, matches a slow method that merges labels', () => {
let seed = 3;
const random = () => (seed = (seed * 48271) % 2147483647) / 2147483647;
for (let k = 0; k < 300; k++) {
const n = 1 + Math.floor(random() * 30);
const edges = Array.from({ length: Math.floor(random() * 41) }, () => [Math.floor(random() * n), Math.floor(random() * n)]);
assert.equal(countComponents(n, edges), slowCount(n, edges), JSON.stringify([n, edges]));
}
});
test('one path through 200,000 vertices, given in shuffled order: one component', () => {
const n = 200000;
let seed = 8;
const random = () => (seed = (seed * 48271) % 2147483647) / 2147483647;
const shuffle = (a) => {
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(random() * (i + 1));
[a[i], a[j]] = [a[j], a[i]];
}
return a;
};
const order = shuffle(Array.from({ length: n }, (_, i) => i));
const edges = shuffle(Array.from({ length: n - 1 }, (_, i) => [order[i], order[i + 1]]));
assert.equal(countComponents(n, edges), 1);
assert.equal(countComponents(n, edges.slice(1)), 2);
}); A hint
Build adjacency lists with both directions of every edge. Then loop over the vertices: each one that no search has reached yet starts a new component, and a search from it marks everything it can reach. Write that search with a list used as a stack (push a vertex when you mark it, pop to explore), so the long path costs no recursion at all.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide, QuickJS): nothing is sent to mysmartcopilot.com. The first run of each language downloads it: Python (about 13.5 MB) or JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Check yourself
5 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
Timestamps in depth: nesting, edge types and cycles, proved and checked Pro: opens with a Pro, Premium or Ultimate pass
The two times of every vertex obey one structural rule, and the four edge kinds follow from it as inequalities. This part proves the rule, classifies the lesson’s edges by their times, and checks every claim on 2,000 random graphs.
In this part:
- The parenthesis rule and the white-path rule, with proofs
- Classifying the lesson’s edges by their times
- Back edges and cycles
1 runnable example (Python) · about 2 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Deep graphs: an explicit stack that keeps the recursive order Pro: opens with a Pro, Premium or Ultimate pass
Recursion is the shortest way to write DFS and the first thing to fail on a long path. This part reproduces the failure, shows why the simple stack version changes the order, and builds a stack version whose timestamps match the recursion exactly, checked on 1,000 random graphs.
In this part:
- What recursion costs on a path of 200,000 vertices, measured
- Why pushing all neighbours changes the order
- A stack of (vertex, position) pairs, checked against recursion
1 runnable example (Python) · about 2 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Components in five languages, and how deep each one can recurse Pro: opens with a Pro, Premium or Ultimate pass
The stack-based search is the same in every interview language, but what happens when you recurse instead differs a great deal. This part counts components in Python, JavaScript, Java, C++ and Go with recorded runs, then measures how each language handles a recursion 200,000 calls deep.
In this part:
- The same search in Python, JavaScript, Java, C++ and Go
- How deep each language can recurse, measured
- Practice set
3 runnable examples (Build and run all three, C++, Go, Java, JavaScript, Python) · about 3 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Interview questions
-
Fresher to senior
An HR system stores a reporting tree of 300,000 employees and must answer 2 million questions a day of the form "does B manage A, directly or through others?", each in constant time after a nightly preparation. How do you prepare, and what does each answer cost?
-
Mid to senior
A platform has 5,000 services and 40,000 "depends on" arcs. When one service fails, a dashboard must list every service that can break because of it, within 10 milliseconds per click. Compare a search per click with precomputing every answer, with numbers.
-
Mid to senior
A scanned map is a 20,000 × 20,000 black-and-white image, 4 × 10⁸ pixels, and you must count its connected black regions on a machine with 2 GB of memory. A recursive flood fill crashes. Design a version that fits, and estimate its memory.
Key takeaways
- DFS goes deep first and backs up, in O(V + E); recursively it is a few lines, iteratively it needs a stack.
- Starting a search from every unmarked vertex counts connected components: one start per component.
- Discovery and finish times nest along the DFS tree, and they sort edges into tree, back, forward and cross edges; a back edge in a directed graph means a cycle.
- Recursion depth can reach V, so use an explicit stack for graphs from input.
References
- Depth-first search and linear graph algorithms (R. E. Tarjan, SIAM Journal on Computing 1(2), 1972) (Society for Industrial and Applied Mathematics (SIAM))
- Dictionary of Algorithms and Data Structures: depth-first search (National Institute of Standards and Technology (NIST))
- Dictionary of Algorithms and Data Structures: connected components (National Institute of Standards and Technology (NIST))
- sys.setrecursionlimit() (Python Software Foundation)
- Java SE 25 API: Thread (the constructor with a stack size) (Oracle)
- Java SE 25 API: StackOverflowError (Oracle)
- Package runtime/debug: SetMaxStack (The Go Authors)
- InternalError: too much recursion (RangeError: Maximum call stack size exceeded) (MDN Web Docs (Mozilla))
- GATE 2027 syllabus for Computer Science and Information Technology (CS) (Indian Institute of Technology Madras (GATE 2027 organising institute))
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress