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.

Coding Interview Patterns  Module 21 – Topological sort and DAGs

Topological order with DFS

Get a topological order from the order in which a depth-first search finishes nodes, catch loops with three states, and handle chains of 100,000 nodes.

  • 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: The problem: an install order for packages · The brute force: install what each package requires, every time · What to notice · 3 more sections. With a pass: 3 in-depth parts and 3 interview answers, about 20 minutes.

What you will learn

  • Derive a topological order from the order in which a depth-first search finishes nodes
  • Distinguish a loop from a node reached twice with three states per node
  • Write the search with a stack of its own for chains deeper than the recursion limit
  • Prove that the finishing order puts every node after what must come before it
  • Choose between Kahn's algorithm and a depth-first search for a given task

Before you start

A depth-first search produces a topological order as a side effect of how it ends each node: it finishes a node only after it has finished everything that node leads to. Follow arrows from each package to the packages it requires, write a package down at the moment it finishes, and the list is an install order in which every package comes after all it requires. Follow arrows the other way, from what comes first to what comes after, and the same list runs backwards, so it is reversed at the end. Three states per node, not reached, on the current path and finished, catch a loop on the way: an arrow back to a node that is still on the path means no order exists. Kept on a stack of its own instead of in nested calls, the search handles a chain of 100,000 packages as easily as six.

The problem: an install order for packages

The problem

requires maps each package’s name to the list of packages that must be installed before it, and a name that appears only inside a list requires nothing. Return every name once, in an order that installs each package after everything it requires, or [] if some packages require each other in a loop, a package that requires itself included. Limits: up to 100,000 names and 200,000 requirements, and one chain of requirements may run through all of them.

Four examples:

  1. The site in the diagram, {"site": ["theme", "markdown"], "theme": ["fonts", "icons"], "icons": ["fonts"], "markdown": ["parser"]}, gives fonts, icons, theme, parser, markdown, site. Nine other orders are valid too, such as parser, fonts, icons, markdown, theme, site.
  2. {"auth": ["session"], "session": ["crypto", "auth"]} gives []: auth and session wait for each other.
  3. {"x": ["x"]} gives []: x waits for itself.
  4. pkg0 requires pkg1, which requires pkg2, and so on to pkg99999: the only order starts at pkg99999.
Six packages with an arrow from each one to what it requires, and in each box when the search from site finishes it.sitefinishes 6ththemefinishes 3rdmarkdownfinishes 5thiconsfinishes 2ndfontsfinishes 1stparserfinishes 4thfonts is done already

The site's packages, numbered in the order the search finishes them

Text description of the diagram

The diagram shows six packages as boxes, drawn from the top down, with an arrow from each package to every package it requires. Each box says when a depth-first search that starts at site finishes that package.

  1. site requires theme and markdown. It finishes 6th, last, because it waits for both.
  2. theme requires fonts and icons, and finishes 3rd.
  3. icons requires fonts, and finishes 2nd. Its arrow to fonts is dashed: when the search reads it, fonts is done already, so there is nothing to do and no loop.
  4. fonts requires nothing. Its box has a double border: it finishes 1st.
  5. markdown requires parser, and finishes 5th.
  6. parser requires nothing, and finishes 4th.

Read in finishing order, the packages are fonts, icons, theme, parser, markdown and site. Every arrow leads from a package to one that finished earlier, so this order installs each package after everything it requires.

The brute force: install what each package requires, every time

The direct code installs a package by first installing each package it requires, the same way, then the package itself unless it is installed already. It is right when there is no loop, but it remembers nothing it has explored:

The brute force, counting its calls Python · brute/naive_install.py
"""The brute force: install what a package requires, recursively, with no memory of what was already explored."""


def naive_order(requires):
    """An install order and the number of calls it took (it never stops on a loop)."""
    order, installed = [], set()
    calls = 0

    def install(p):
        nonlocal calls
        calls += 1
        for r in requires.get(p, ()):
            install(r)                  # explored again, however often it was explored before
        if p not in installed:
            installed.add(p)
            order.append(p)

    for p in requires:
        install(p)
    return order, calls


def diamonds(k):
    """top requires a1 and b1, both require m1, m1 requires a2 and b2, ..., down to mk."""
    requires = {"top": ["a1", "b1"]}
    for i in range(1, k + 1):
        requires[f"a{i}"] = [f"m{i}"]
        requires[f"b{i}"] = [f"m{i}"]
        requires[f"m{i}"] = [f"a{i + 1}", f"b{i + 1}"] if i < k else []
    return requires


site = {
    "site": ["theme", "markdown"],
    "theme": ["fonts", "icons"],
    "icons": ["fonts"],
    "markdown": ["parser"],
}
order, calls = naive_order(site)
print("site:", " ".join(order), f"({calls} calls for {len(order)} packages)")

print("\nA chain of diamonds: every level doubles the calls")
for k in (1, 2, 4, 8, 12, 16):
    order, calls = naive_order(diamonds(k))
    assert calls == 2 ** (k + 4) - 7 * k - 15     # the exact count, so it can be worked out for larger k
    print(f"{k:2} diamonds: {len(order):2} packages, {calls:>9,} calls")

loop = {"auth": ["session"], "session": ["crypto", "auth"]}
try:
    naive_order(loop)
except RecursionError:
    print("\nauth and session require each other: the calls never end, and Python stops them with RecursionError")

Output

site: fonts icons theme parser markdown site (15 calls for 6 packages)

A chain of diamonds: every level doubles the calls
 1 diamonds:  4 packages,        10 calls
 2 diamonds:  7 packages,        35 calls
 4 diamonds: 13 packages,       213 calls
 8 diamonds: 25 packages,     4,025 calls
12 diamonds: 37 packages,    65,437 calls
16 diamonds: 49 packages, 1,048,449 calls

auth and session require each other: the calls never end, and Python stops them with RecursionError

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 naive_install.py

It makes 15 calls for the site’s 6 packages. In a chain of diamonds, where two packages of each level require the same package below them, every level doubles the calls, and the run checks each count against 2k+4 − 7k − 15. Thirty diamonds, 91 packages, would need about 1.7 × 10¹⁰ calls, roughly 30 minutes at the CPython rate assumed in the lesson on constraints. On a loop the calls never end: auth calls session, which calls auth, until Python raises RecursionError.

What to notice

  • A package’s place depends only on what it requires, so once it is done, exploring it again changes nothing: remember it.
  • Arriving at a package that is entered but not finished means a loop, and a single “seen” mark cannot tell that from arriving at a finished one.

The key idea: a package is done when its requirements are

Give each package one of three states: new, open (entered and not finished, so it is on the current path) and done. Start a search at every package that is still new. The search opens a package and reads its requirements one by one:

  • a new requirement is opened and searched first;
  • a done requirement is in the order already, so there is nothing to do;
  • an open requirement is on the current path, so it requires the package being read, directly or through others: that is a loop, and the answer is [].

When its last requirement has been read, the package is done and goes to the end of the order, after everything it requires. The arrows here point from a package to what must come before it. With arrows from what comes first to what comes after, as in the lesson on Kahn’s algorithm, a node finishes after everything that must follow it, and that list must be reversed. Reversing a list that is already right gives one that is exactly backwards.

A recursive function is the shortest way to write the search, but a chain of 100,000 packages would need 100,000 nested calls. Keep the path in a list instead, and beside each package on it the position of the next requirement to read: the package on top decides the next step, and popping it returns to the one below where it stopped. Each name is opened once and each requirement read once, so the search costs O(n + m) time for n names and m requirements, and O(n) memory beside the input.

A dry run on the site

Every step on the site, then on auth and session Python · trace/trace_dfs.py
"""Every step of the depth-first search on two small sets of packages: what it reads, the path, and the order so far."""

NEW, OPEN, DONE = "new", "open", "done"


def trace(requires):
    state, order = {}, []
    for root in requires:
        if state.get(root, NEW) != NEW:
            continue
        state[root] = OPEN
        stack = [[root, 0]]                      # [package, position of the next requirement to read]
        print(f"  start at {root:<22} path: {root}")
        while stack:
            top = stack[-1]
            p, i = top
            needs = requires.get(p, [])
            if i == len(needs):
                stack.pop()
                state[p] = DONE
                order.append(p)
                print(f"  {p + ' is done':<31} order: {' '.join(order)}")
                continue
            top[1] += 1
            r = needs[i]
            s = state.get(r, NEW)
            step = f"{p} needs {r}"
            if s == DONE:
                print(f"  {step:<31} {r} is done already: nothing to do")
            elif s == OPEN:
                loop = [q for q, _ in stack[[q for q, _ in stack].index(r):]] + [r]
                print(f"  {step:<31} {r} is open, on the path: a loop, {' > '.join(loop)}")
                return []
            else:
                state[r] = OPEN
                stack.append([r, 0])
                print(f"  {step:<31} path: {' > '.join(q for q, _ in stack)}")
    return order


site = {
    "site": ["theme", "markdown"],
    "theme": ["fonts", "icons"],
    "icons": ["fonts"],
    "markdown": ["parser"],
}
print("The site:")
print("Install order:", trace(site))

print("\nauth and session:")
print("Install order:", trace({"auth": ["session"], "session": ["crypto", "auth"]}))

Output

The site:
  start at site                   path: site
  site needs theme                path: site > theme
  theme needs fonts               path: site > theme > fonts
  fonts is done                   order: fonts
  theme needs icons               path: site > theme > icons
  icons needs fonts               fonts is done already: nothing to do
  icons is done                   order: fonts icons
  theme is done                   order: fonts icons theme
  site needs markdown             path: site > markdown
  markdown needs parser           path: site > markdown > parser
  parser is done                  order: fonts icons theme parser
  markdown is done                order: fonts icons theme parser markdown
  site is done                    order: fonts icons theme parser markdown site
Install order: ['fonts', 'icons', 'theme', 'parser', 'markdown', 'site']

auth and session:
  start at auth                   path: auth
  auth needs session              path: auth > session
  session needs crypto            path: auth > session > crypto
  crypto is done                  order: crypto
  session needs auth              auth is open, on the path: a loop, auth > session > auth
Install order: []

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 trace_dfs.py

fonts is finished first, so it is installed first. When icons reads fonts again, fonts is done: a package reached twice is not a loop. On the second input, session reads auth while auth is still open below it on the path: a loop.

Exercise: an install order for any input

Exercise · Medium · Python

Install every package after what it requires

install_order(requires) in install.py gets a dictionary that maps package names to the lists of packages they require. A name that appears only inside a list requires nothing. Return a list of every name, each exactly once, in which every package comes after everything it requires. Return [] when some packages require each other in a loop, a package that requires itself included.

install_order({"app": ["lib", "util"], "util": ["lib"]})   # ['lib', 'util', 'app']
install_order({"auth": ["session"], "session": ["auth"]})  # []: a loop
install_order({"x": [], "y": []})                          # ['x', 'y'], or ['y', 'x']

Any valid order is accepted. The starter code is a recursive search that marks each package "seen". It gets small inputs without loops right, but it returns an order for a loop instead of [], and one sample test is a chain of 100,000 packages, far deeper than Python's default recursion limit of 1,000 frames. Make it report loops, then make it work without recursion.

Starter code · install.py

def install_order(requires):
    """Every package after everything it requires, or [] when some packages require each other in a loop."""
    # This search is right on small inputs without loops. It misses loops, and it recurses once per package.
    seen = set()
    order = []

    def visit(p):
        seen.add(p)
        for r in requires.get(p, []):
            if r not in seen:
                visit(r)
        order.append(p)

    for p in requires:
        if p not in seen:
            visit(p)
    return order
The sample tests · test_install.py
import copy
import random

from install import install_order


def names_of(requires):
    return list(dict.fromkeys([*requires, *(r for rs in requires.values() for r in rs)]))


def has_loop(requires):
    """Installs, again and again, any package whose requirements are all installed: slow, but hard to get wrong."""
    names = names_of(requires)
    done = set()
    while True:
        ready = [p for p in names if p not in done and all(r in done for r in requires.get(p, []))]
        if not ready:
            return len(done) < len(names)
        done.update(ready)


def check(requires):
    got = install_order(requires)
    if has_loop(requires):
        assert got == [], f"install_order({requires}) returned {got}, but some packages require each other in a loop"
        return
    names = names_of(requires)
    assert sorted(got) == sorted(names), f"install_order({requires}) returned {got}, not every package exactly once"
    at = {p: i for i, p in enumerate(got)}
    for p, rs in requires.items():
        for r in rs:
            assert at[r] < at[p], f"install_order({requires}) returned {got}, which puts {p} before {r}"


def test_the_examples():
    """the three calls of the prompt"""
    assert install_order({"app": ["lib", "util"], "util": ["lib"]}) == ["lib", "util", "app"]
    assert install_order({"auth": ["session"], "session": ["auth"]}) == []
    assert sorted(install_order({"x": [], "y": []})) == ["x", "y"]


def test_the_site():
    """the lesson's site: fonts is reached twice, which is not a loop"""
    check({"site": ["theme", "markdown"], "theme": ["fonts", "icons"], "icons": ["fonts"], "markdown": ["parser"]})


def test_loops_of_every_size():
    """a package that requires itself, a loop of two behind a package, a loop of four that the first package misses"""
    check({"x": ["x"]})
    check({"cart": ["auth"], "auth": ["session"], "session": ["crypto", "auth"]})
    check({"a": [], "b": ["c"], "c": ["d"], "d": ["e"], "e": ["b"]})


def test_repeats_and_empty_input():
    """a requirement listed twice, a package with an empty list, and no packages at all"""
    check({"app": ["lib", "lib"]})
    check({"app": [], "lib": ["app"]})
    assert install_order({}) == []


def test_the_input_is_left_unchanged():
    """the function reads the dictionary and changes nothing in it"""
    requires = {"a": ["b"], "b": ["c"]}
    before = copy.deepcopy(requires)
    install_order(requires)
    assert requires == before


def test_random_sets_of_packages():
    """400 random sets of up to 8 packages, half of them made without a loop"""
    rng = random.Random(11)
    for t in range(400):
        names = [f"p{i}" for i in range(rng.randint(1, 8))]
        requires = {}
        for i, p in enumerate(names):
            pool = names[:i] if t % 2 == 0 else names
            if pool:
                requires[p] = [rng.choice(pool) for _ in range(rng.randint(0, 3))]
            elif rng.random() < 0.5:
                requires[p] = []
        keys = list(requires)
        rng.shuffle(keys)
        check({p: requires[p] for p in keys})


def test_a_chain_of_100000():
    """pkg0 requires pkg1, which requires pkg2, ... down to pkg99999; then the same chain closed into a loop"""
    n = 100_000
    chain = {f"pkg{i}": [f"pkg{i + 1}"] for i in range(n - 1)}
    got = install_order(chain)
    assert len(got) == n and got[0] == "pkg99999" and got[-1] == "pkg0", "the chain must be installed from its end"
    chain["pkg99999"] = ["pkg0"]
    assert install_order(chain) == [], "the chain closed into a loop has no install order"
A hint

"Seen" mixes two different states. Give each package one of three: not reached yet, on the current path of the search, or done. A requirement that is on the current path closes a loop; one that is done is already in the order. For the deep chain, keep the path in a list of your own, and beside each package on it the position of the next requirement to read, so that you can come back to it where you left off.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 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.

Python Online Compiler Paste the brute force, add one more diamond to the chain and watch the count of calls double.

Check yourself

4 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 4 The search follows arrows from each package to the packages it requires. When does a package join the install order?

    Choose one answer.

    Show the answer to question 1

    Answer: When every package it requires is done

    A package is done once the search has finished every package it requires, and only then is it appended. So each package lands after all its requirements, and the finishing order is an install order. Appending on arrival puts a package before what it needs.

  2. Question 2 of 4 The search is at icons and reads icons → fonts. fonts is done already. What does that tell you?

    Choose one answer.

    Show the answer to question 2

    Answer: Nothing is wrong: fonts is already in the order, before icons

    A done package and everything it requires are in the order already, so icons will follow it. Only an arrow to a package that is still open, on the current path, closes a loop. fonts is reached twice in the site, and the site has no loop.

  3. Question 3 of 4 In another graph the arrows point from what must come first to what comes after: a → b means a before b. What does the search do with its finishing order?

    Choose one answer.

    Show the answer to question 3

    Answer: Reverses it

    Following such arrows, a node finishes after everything that must come after it, so the finishing order runs backwards and is reversed at the end. Following arrows to requirements, as in this lesson, a package finishes after what must come before it, so no reversal is needed.

  4. Question 4 of 4 For which inputs does install_order return []?

    Choose every answer that is right.

    Show the answer to question 4

    Answer:

    • {"x": ["x"]}
    • {"a": ["b"], "b": ["a"]}

    A package that requires itself is a loop of one, and a and b that require each other are a loop of two: neither can ever be installed. A requirement listed twice is still one requirement, so the third input gives b, a, and the fourth gives a, b.

Why the finishing order is valid, with the code in five languages Pro: opens with a Pro, Premium or Ultimate pass

The order is right for a reason that fits in a few lines. Below is that argument, then the search in Python, JavaScript, Java, C++ and Go with their recorded output, and a check on 3,000 random inputs.

In this part:

  • Why every package finishes after what it requires: the proof
  • The code in five languages, with their recorded output
  • Checked against Kahn’s algorithm and graphlib on 3,000 random inputs

3 runnable examples (C++, Go, Java, JavaScript, Python) · about 4 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 chains measured in five languages, and six bugs found Pro: opens with a Pro, Premium or Ultimate pass

A recursive version passes small tests and fails on deep inputs, and where it fails depends on the language. These runs measure it, then run six plausible bugs against every input of up to three packages.

In this part:

  • What recursion costs on a chain of 100,000 packages, measured
  • Six bugs, each on the smallest input that breaks it

3 runnable examples (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).

Kahn or DFS, reporting the loop, and a practice set Pro: opens with a Pro, Premium or Ultimate pass

Kahn’s algorithm and the depth-first search reach the same kind of order with different strengths. A trade-off table, three variants that run and a practice set with answers show which one fits a task.

In this part:

  • Kahn’s algorithm or a depth-first search: the trade-offs
  • Reporting the loop, installing one package, and graphlib
  • Why an alphabetical search is not the alphabetically first order
  • A practice set with answers

1 runnable example (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 mid · Warm-up

    When does a depth-first topological sort reverse its finishing order, and when must it not?

    It depends on which way the arrows point. If an arrow a → b means "a comes before b", the search finishes a node only after everything reachable from it, which is everything that must come after it, so the nodes finish in reverse topological order and the list is reversed at the end. If each arrow points from a node to something that must come before it, as a package's list of requirements does, a node finishes after everything it requires, so the finishing order is already valid and must not be reversed: reversing it gives an order that is exactly backwards. The search, the three states and the O(V + E) cost are the same either way; only the last step depends on the direction.

  2. Mid to senior

    Your recursive depth-first install order passes every test, then crashes in production on a chain of 60,000 packages, each requiring the next. Why does it crash, and how do you change the code without changing the order it returns?

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

  3. Fresher to mid

    A teammate's depth-first install order marks each package "seen" and appends it after its requirements. It passes all the review tests. Which input makes it return a wrong answer, and what is the smallest change that fixes it?

    Model answer, Pro. First step and follow-ups

  4. Mid to senior

    A registry holds 2 million packages and 9 million requirements. A user asks to install one package. How do you work out what to install and in which order, and what does it cost compared with ordering the whole registry?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • Following arrows to requirements, the order in which a depth-first search finishes packages is an install order; with arrows from what comes first to what comes after, reverse it.
  • Three states, new, open and done, tell a loop, an arrow to an open package, from a package reached twice.
  • Keep the path and each package’s next position on a stack of your own: O(n + m) time, and no recursion limit to hit.

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.