📁 Sangam Dhakal - Portfolio v2.0 - Netscape Navigator 4.0
_
□
×

📚 Interview question

Explain depth-first search (DFS). Implement it recursively and iteratively in C#, and show how DFS detects cycles in a directed graph and produces a topological order, for example for build or migration dependencies.

Depth-first search: recursive and iterative implementations, pre/post order, three-colour cycle detection, topological sort, connected components and backtracking.

Mid-Level Coding Advanced algorithms cycle-detection dfs graph-traversal graphs topological-sort

🎯 Key points

  • DFS explores one branch as deep as possible before backtracking, using recursion or an explicit `Stack<T>`, plus a visited set
  • Time is O(V + E); space is O(V) for the visited set plus O(D) for the stack, where D is the deepest path
  • Pre-order processes a node on entry (printing a tree); post-order processes it on finish (folder sizes, deleting a tree, topological sort)
  • Directed cycle detection needs three states: white (unvisited), grey (on the current path), black (finished); an edge to a grey node is a back edge and means a cycle
  • A plain visited set gives false cycles on diamonds (A→B→D, A→C→D); in undirected graphs a cycle is an edge to a visited node other than the parent
  • Topological order is DFS post-order (dependencies first); Kahn's algorithm (in-degrees with a queue) gives the same order and suits level-by-level parallel execution
  • Use iterative DFS with per-node enumerators for deep or untrusted graphs, because a stack overflow terminates a .NET process
  • Start DFS from every unvisited node, and report the full cycle path; this is how build tools, migration runners and DI containers report circular dependencies

⏱ 30-second answer

DFS goes as deep as possible along one branch before backtracking. It uses recursion or an explicit stack, plus a visited set. O(V + E) time, O(V) space (the longest path).

Key uses:

  • Cycle detection in directed graphs with three states: unvisited, in progress (on the current path), done. Reaching an in-progress node means a back edge, so a cycle.
  • Topological sort: add each node to a list after all its dependencies finish (post-order); that list is a valid build order.
  • Connected components, path existence, backtracking (permutations, sudoku).

🗣 Full answer (senior)

How DFS moves

        A
      /   \
     B     C
    / \     \
   D   E     F

DFS from A (children left to right):
A -> B -> D (dead end, back to B) -> E (back to B, back to A) -> C -> F
pre-order : A B D E C F   (when first entered)
post-order: D E B F C A   (when finished)

DFS is the natural shape of recursion: call Visit(child) for each child. Iteratively, push neighbours on a Stack<T>.

Pre-order versus post-order

  • Pre-order (process on entry): printing a folder tree, copying a structure.
  • Post-order (process on exit): computing folder sizes (children first), deleting a tree, topological sort.

Cycle detection in a directed graph

A plain visited set is not enough: in a diamond A → B → D, A → C → D, reaching D twice is not a cycle. You need to know whether a node is on the current path:

State Meaning
White (0) not visited yet
Grey (1) in progress, on the current recursion path
Black (2) finished, all descendants explored

An edge to a grey node is a back edge, so a cycle. An edge to a black node is fine.

Topological sort with DFS

For dependencies "A depends on B", visit B before finishing A. When a node turns black, append it to the output. The output lists dependencies before dependants. This is how build systems (MSBuild project references), package managers, migration runners and DI containers order work, and how they report "circular dependency detected" with the actual cycle.

Kahn's algorithm (BFS with in-degrees) gives the same result; DFS is shorter and naturally yields the cycle path, while Kahn's makes it easy to process independent items in parallel by levels.

BFS versus DFS

Question Use
Fewest hops / nearest BFS
Is there any path? either (DFS uses less memory on wide graphs)
Cycle detection, topological order DFS (or Kahn)
Explore all combinations (backtracking) DFS
Connected components either

Production example

Our migration runner let each data migration declare DependsOn. Someone added a dependency that created a cycle (AddInvoiceIndex → BackfillInvoices → AddInvoiceIndex) and the runner hung in an infinite loop at deploy time. We rewrote ordering as a DFS topological sort with three colours; on a back edge it throws with the full cycle path (AddInvoiceIndex → BackfillInvoices → AddInvoiceIndex), and a unit test runs the sort over all registered migrations so the cycle fails the build instead of the deploy.

🔍 What the interviewer is checking

Checks DFS implementation skill and understanding of its main applications.

Green flags

  • Implements DFS recursively and iteratively.
  • Explains pre-order versus post-order.
  • Uses three colours for directed cycle detection and explains back edges.
  • Produces a topological order via post-order and reports the cycle.
  • Mentions stack depth risk in .NET and real tools that use topological sort.

Red flags

  • Detects directed cycles with a plain visited set.
  • Emits topological order in pre-order.
  • Claims DFS finds shortest paths.
  • Only starts from one node in a disconnected graph.

📚 Learn in depth

1. Recursive DFS and the call stack

void Visit(Node n)
{
    if (!visited.Add(n)) return;
    Pre(n);
    foreach (var m in n.Next) Visit(m);
    Post(n);
}

The recursion depth equals the length of the current path. A chain of 100,000 nodes means 100,000 frames, which can overflow the 1 MB default thread stack, and a StackOverflowException kills the .NET process. For untrusted or very deep graphs, use the iterative form.

2. Iterative DFS that still gives post-order

A simple Stack<T> gives pre-order easily. For post-order (needed for topological sort) push a marker or keep an enumerator per frame:

stack of (node, enumerator over its neighbours)
push (start, start.Next.GetEnumerator())
while stack not empty:
    (n, it) = peek
    if it.MoveNext():
        m = it.Current
        if m is white: colour grey, push (m, m.Next.GetEnumerator())
        else if m is grey: cycle!
    else:
        pop; colour n black; output.Add(n)   <- post-order

This mirrors exactly what the recursive version does with the call stack, but the stack lives on the heap.

3. Edge classification

During DFS on a directed graph every edge u → v is one of:

Edge v's colour when seen Meaning
Tree edge white first discovery
Back edge grey cycle
Forward edge black, v descendant of u fine
Cross edge black, other branch fine

Only back edges matter for cycle detection. In undirected graphs, a cycle is any edge to a visited node that is not the parent you came from.

4. Topological sort worked through

Dependencies (X -> Y means X depends on Y):
  Api   -> Domain, Infra
  Infra -> Domain
  Tests -> Api

DFS from Tests: Tests(grey) -> Api(grey) -> Domain(grey, no deps) -> black, output [Domain]
                                       -> Infra(grey) -> Domain(black, skip) -> black, output [Domain, Infra]
                 Api black, output [Domain, Infra, Api]
Tests black, output [Domain, Infra, Api, Tests]
Build order: Domain, Infra, Api, Tests

If edges point the other way ("X is needed by Y"), reverse the final list.

5. Recovering the cycle path

Keep a parent map or the current path stack. When you hit a grey node g, walk the path back from the current node until g:

path stack: [Tests, Api, Infra, Shared]
edge Shared -> Api (Api is grey)
cycle: Api -> Infra -> Shared -> Api

Reporting the cycle is what turns a hang into a fixable error message.

6. Backtracking is DFS over a decision tree

Generating permutations, solving sudoku, or exploring configuration combinations is DFS where each level is a choice. Undo the choice when you return (backtrack). Prune branches early when constraints already fail; that pruning is what makes backtracking usable.

7. Try it yourself

var deps = new Dictionary<string, string[]>
{
    ["Tests"] = ["Api"],
    ["Api"] = ["Domain", "Infra"],
    ["Infra"] = ["Domain"],
    ["Domain"] = [],
};
Console.WriteLine(string.Join(" -> ", TopoSort.Order(deps)));   // Domain -> Infra -> Api -> Tests

deps["Domain"] = ["Tests"];                                      // introduce a cycle
try { TopoSort.Order(deps); }
catch (InvalidOperationException ex) { Console.WriteLine(ex.Message); }

Use the TopoSort class from the code example.

💻 Code

public static class TopoSort
{
    private enum Colour : byte { White, Grey, Black }

    // deps[x] = things x depends on. Returns dependencies first. Iterative: no stack overflow.
    public static List<string> Order(IReadOnlyDictionary<string, string[]> deps)
    {
        var colour = new Dictionary<string, Colour>();
        var output = new List<string>(deps.Count);
        var path = new Stack<(string Node, IEnumerator<string> Next)>();

        foreach (var root in deps.Keys)
        {
            if (colour.GetValueOrDefault(root) != Colour.White) continue;
            colour[root] = Colour.Grey;
            path.Push((root, Neighbours(deps, root)));

            while (path.Count > 0)
            {
                var (node, next) = path.Peek();
                if (next.MoveNext())
                {
                    var dep = next.Current;
                    switch (colour.GetValueOrDefault(dep))
                    {
                        case Colour.White:
                            colour[dep] = Colour.Grey;
                            path.Push((dep, Neighbours(deps, dep)));
                            break;
                        case Colour.Grey:
                            throw new InvalidOperationException("Circular dependency: " + DescribeCycle(path, dep));
                        // Black: already ordered, nothing to do
                    }
                }
                else
                {
                    path.Pop();
                    colour[node] = Colour.Black;
                    output.Add(node);            // post-order: after all its dependencies
                }
            }
        }
        return output;
    }

    private static IEnumerator<string> Neighbours(IReadOnlyDictionary<string, string[]> deps, string n) =>
        ((IEnumerable<string>)(deps.TryGetValue(n, out var d) ? d : [])).GetEnumerator();

    private static string DescribeCycle(Stack<(string Node, IEnumerator<string> Next)> path, string start)
    {
        var nodes = path.Select(p => p.Node).Reverse().ToList();   // bottom of stack first
        var cycle = nodes.Skip(nodes.IndexOf(start)).Append(start);
        return string.Join(" -> ", cycle);
    }
}

// Recursive DFS for comparison: connected components in an undirected graph
public static int CountComponents(Dictionary<int, List<int>> g)
{
    var seen = new HashSet<int>();
    int count = 0;
    foreach (var v in g.Keys)
        if (seen.Add(v)) { count++; Dfs(v); }
    return count;

    void Dfs(int u)
    {
        foreach (var w in g[u])
            if (seen.Add(w)) Dfs(w);   // recursion depth up to V: fine for small graphs only
    }
}

⚠ Common mistakes

  • Mistake: Using a single visited set to detect cycles in a directed graph. Why it is wrong: In a diamond (A→B→D and A→C→D), D is reached twice without any cycle, so you report false cycles. Instead: Use three states (white, grey, black); an edge to a grey node is a cycle. Keep the current path so you can report which nodes form it.

  • Mistake: Adding nodes to the topological order on entry (pre-order). Why it is wrong: A node is emitted before its dependencies have finished, so the order is invalid. Instead: Emit on finish (post-order), and reverse the list if your edges point from dependency to dependant.

  • Mistake: Recursive DFS on large or untrusted graphs. Why it is wrong: Recursion depth equals path length; a long chain overflows the thread stack, and .NET cannot catch StackOverflowException, so the process dies. Instead: Use an explicit stack with a per-node enumerator, which keeps the same post-order behaviour on the heap.

  • Mistake: Reporting only "cycle detected". Why it is wrong: Engineers cannot tell which of hundreds of dependencies is wrong, so the fix takes hours. Instead: Track the current path and report the full cycle, such as Api -> Infra -> Shared -> Api.

  • Mistake: Using DFS to find the shortest path. Why it is wrong: DFS returns the first path it finds, which can be very long. Instead: Use BFS for unweighted graphs or Dijkstra for weighted graphs.

  • Mistake: Starting DFS from only one node. Why it is wrong: Nodes in other components are never visited, so a cycle there is missed and those nodes are missing from the topological order. Instead: Loop over every node and start a DFS from each one that is still unvisited.

🏛 Senior insight: trade-offs

A senior engineer recognises DFS-based ordering inside everyday tools: MSBuild ordering project references, NuGet resolving packages, the .NET DI container reporting a circular dependency, EF Core migrations, and workflow engines. When they build something with dependencies, they add a topological sort with cycle reporting and run it in a unit test, so a bad dependency fails CI rather than hanging a deploy.

Need Choice Why
Build or migration order with clear cycle errors DFS post-order, three colours Short code, gives the exact cycle path
Run independent tasks in parallel by level Kahn's algorithm Each wave of zero in-degree nodes can run together
Deep or data-driven graphs Iterative DFS No stack overflow risk in .NET
Small trusted trees Recursive DFS Clearest code
Explore combinations (permutations, configurations) Backtracking (DFS with pruning) Prune early or cost is exponential
Shortest hops BFS, not DFS DFS gives no distance guarantee

They also treat a detected cycle as a design signal, not only an error: two modules that depend on each other usually need an interface or a shared lower layer extracted.

🔗 Follow-ups the interviewer may ask

  1. When: Interviewer asks for the shortest path instead Why: Checks they switch to BFS
    How to answer

    Graph traversal means visiting every node reachable from a start node exactly once, using a visited set so cycles do not loop forever. The two basic orders are BFS (breadth-first, uses a queue) and DFS (depth-first, uses a stack or recursion).

    BFS visits nodes in layers of distance 0, 1, 2, ... so the first time it reaches a node is via the fewest edges: it finds shortest paths in unweighted graphs. O(V + E) time, O(V) space. Uses: degrees of separation, nearest available item, impact of a change in a dependency graph, web crawling by depth.

  2. When: Candidate mentions Kahn's algorithm Why: Compares DFS ordering with in-degree based topological sort
    How to answer

    A graph is a set of vertices (nodes) connected by edges. Edges can be directed or undirected, weighted or not, and the graph may contain cycles. Trees are a special case (connected, no cycles).

    Representations:

    • Adjacency list (Dictionary<TNode, List<TNode>>): O(V + E) memory, best for sparse graphs (most real ones).
    • Adjacency matrix (bool[V,V]): O(V²) memory, O(1) edge check, for small dense graphs.
    • Edge list: simple, good for storage and some algorithms.

    Backend graphs: service and package dependencies, DI containers, build systems (project references), workflow/job DAGs, permission inheritance, routing, social links.

    For dependencies you need a DAG: find an execution order with topological sort (Kahn's algorithm, O(V + E)); if not all nodes can be ordered, there is a cycle.

  3. When: Candidate writes recursive DFS Why: Checks stack depth risk and iterative conversion
    How to answer

    Recursion is a function solving a problem by calling itself on a smaller version of it. Every recursive function needs:

    1. A base case that stops.
    2. A recursive case that moves towards the base case.

    Each call pushes a frame on the thread's call stack (about 1 MB by default on Windows). Too deep and you get a StackOverflowException, which cannot be caught and kills the process. Recursion fits naturally recursive data (trees, nested JSON, folders), but use a loop with an explicit Stack<T> or Queue<T> when depth can be large or is controlled by input.

🛠 Real-world problems: how you handle them

Deployment hung on a circular migration dependency
Situation
A custom data-migration runner ordered [~120] migrations using declared DependsOn attributes. A new migration was added in a feature branch.
Signals
The release pipeline's migration step never finished and timed out after [30 minutes]; CPU on the runner was 100%; no error logged.
Investigation
Attached dotnet-dump and looked at the stacks: the ordering loop kept re-adding the same two migrations. Printing the dependency map showed AddInvoiceIndex depended on BackfillInvoices, which depended on AddInvoiceIndex.
Resolution
Fixed the dependency, then replaced the ordering logic with an iterative DFS topological sort using white/grey/black states that throws with the full cycle path.
Prevention
A unit test runs the sort over every registered migration so cycles fail the PR build; code review checklist item for DependsOn changes.

How to tell it (STAR)

At [company], a release hung at the migration step with no error. I took a dump and saw the ordering loop spinning on two migrations that depended on each other. I fixed the dependency, then rewrote ordering as a DFS topological sort with three states, so it now fails fast with a message like 'AddInvoiceIndex -> BackfillInvoices -> AddInvoiceIndex'. I added a unit test over all migrations, and since then cycles get caught in the PR rather than in a deploy.

↩ Usually asked after

More in Algorithms

Last updated 23 Oct 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me