Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Intermediate
  • 30 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7, Node.js 24.21.0, quickjs 0.32.0, Java 25, Clang 21.0.0 and Go 1.27.1
  • By MySmartCoPilot
Free core, Pro depth: 10 of 30 minutes free
Free: Going deep, then backing up · Counting components · Recursion or an explicit stack · Exercise and self-check. With a pass: 3 in-depth parts and 3 interview answers, about 20 minutes.

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

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 DFS with discovery and finish times Python · trace/dfs.py
# 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

A directed graph of 7 vertices after DFS from 0: each vertex shows its discovery and finish time; six tree arcs and four dashed arcs.0 · 1 / 141 · 2 / 72 · 3 / 63 · 4 / 54 · 8 / 135 · 9 / 126 · 10 / 11back, 2 to 0back, 6 to 4forward, 0 to 3cross, 4 to 3

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:

Seven islands found by stack-based DFS Python · islands/islands.py
# 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

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.

Depth-first search on V vertices and E edges, with adjacency lists
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.

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.

  1. Question 1 of 5 A DFS that starts from one vertex reaches all 9 vertices of a graph. What is the largest finish time on the clock, counting from 1?

    Type a number.

    Show the answer to question 1

    Answer: 18

    The clock ticks once when each vertex is discovered and once when it is finished: 2 × 9 = 18 ticks, and the first vertex discovered is the last to finish, at 18.

  2. Question 2 of 5 A graph has 12 vertices, 4 of them isolated and the other 8 forming two separate cycles of 4. How many times does the outer loop start a new search?

    Type a number.

    Show the answer to question 2

    Answer: 6 searches

    One search per connected component: 4 isolated vertices and 2 cycles make 6 components.

  3. Question 3 of 5 In one DFS, vertex u has times 3 / 12 and vertex v has times 5 / 8. What follows?

    Choose one answer.

    Show the answer to question 3

    Answer: v is a descendant of u in the DFS tree

    The interval 5 to 8 lies inside 3 to 12, so v was discovered after u and finished before u: the search found v while u was still open, which makes v a descendant of u.

  4. Question 4 of 5 Why does a recursive DFS fail in Python on a path of 50,000 vertices, while a stack-based one does not?

    Choose one answer.

    Show the answer to question 4

    Answer: Each recursive call waits for the next, so the recursion goes 50,000 calls deep, past the default limit of 1,000

    Recursion keeps one frame per vertex on the current path. CPython stops at sys.getrecursionlimit(), 1,000 by default; a list used as a stack lives on the heap and can hold all 50,000 vertices.

  5. Question 5 of 5 DFS of a directed graph meets an edge u → v while v is still open. What does that show?

    Choose one answer.

    Show the answer to question 5

    Answer: The graph has a directed cycle through u and v

    v is open, so it is an ancestor of u: the tree path from v down to u plus the edge u → v closes a cycle. Such an edge is a back edge, and the cycle-detection lesson builds on it.

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.

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.

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.

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

  1. 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?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

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

    Model answer, Pro. First step and follow-ups

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

    Model answer, Pro. First step and follow-ups

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

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.