Topological Sort: Ordering Directed Acyclic Graphs

Learn topological sorting using DFS and Kahn's algorithm for dependency resolution in directed acyclic graphs.

published: reading time: 19 min read author: GeekWorkBench updated: June 17, 2026
Quick Summary

Topological sort orders vertices in a DAG so every directed edge points from earlier to later—essential for build systems, course prerequisites, and package installation order. Only DAGs have topological orderings; if a cycle exists, no valid order exists, which makes topological sort also a cycle detection tool. Two approaches: Kahn's algorithm uses BFS with in-degree counting, removing nodes with zero in-degree first; DFS-based sorts push nodes onto the result stack after visiting all their dependents. Both run in O(V + E). After reading this you'll apply topological sort to dependency resolution and detect cycles in directed graphs.

Topological Sort: Ordering Directed Acyclic Graphs

When you need to order tasks respecting their dependencies—like build systems, course schedules, or package installations—topological sort is the algorithm that delivers. It produces a linear ordering of vertices in a DAG (Directed Acyclic Graph) where every directed edge points from an earlier to a later vertex in the order.

The key insight is that only DAGs admit topological orderings—if a graph has a cycle, you can’t order vertices such that all edges go “forward,” because the cycle creates a logical impossibility. This property makes topological sort also a cycle detection mechanism for directed graphs.

Introduction

Topological sort produces a linear ordering of vertices in a Directed Acyclic Graph (DAG) where every directed edge points from earlier to later in the order. This matters because many real-world problems involve dependencies—build systems where task B requires task A, course schedules where CS101 must be taken before CS201, or package managers resolving installation order. If you can represent your dependencies as a DAG, topological sort tells you exactly how to order things so all prerequisites are satisfied.

The key insight: only DAGs admit topological orderings. A graph with a cycle can’t be ordered so all edges go “forward”—the cycle creates a logical contradiction. Conveniently, this means topological sort doubles as cycle detection: if you can’t produce a full ordering, cycles exist.

Two main approaches exist: Kahn’s algorithm (BFS-based, uses indegree counting) and DFS-based post-ordering. Both run in O(V + E) time. This guide covers both, explains why DFS produces vertices in reverse order, how Kahn’s handles edge cases, and connects the algorithm to other graph problems and interview patterns.

Kahn’s Algorithm (BFS-based)

The most intuitive approach uses indegree counting: repeatedly remove vertices with zero indegree (no dependencies), updating the graph as we go.

from collections import deque, defaultdict

def topological_sort_kahn(num_vertices, edges):
    """
    Kahn's algorithm for topological sort.

    Args:
        num_vertices: Number of vertices (0 to num_vertices-1)
        edges: List of (u, v) directed edges from u to v

    Returns:
        Topological ordering list, or empty list if cycle exists
    """
    # Build adjacency list and indegree count
    graph = defaultdict(list)
    indegree = [0] * num_vertices

    for u, v in edges:
        graph[u].append(v)
        indegree[v] += 1

    # Start with all vertices that have no dependencies
    queue = deque([i for i in range(num_vertices) if indegree[i] == 0])
    result = []

    while queue:
        # Process vertex with no remaining dependencies
        vertex = queue.popleft()
        result.append(vertex)

        # Reduce indegree of neighbors
        for neighbor in graph[vertex]:
            indegree[neighbor] -= 1
            if indegree[neighbor] == 0:
                queue.append(neighbor)

    # If result doesn't contain all vertices, there's a cycle
    if len(result) != num_vertices:
        return []  # Cycle detected

    return result

DFS-based Topological Sort

A recursive DFS approach finishes vertices in reverse order of when they were visited:

def topological_sort_dfs(num_vertices, edges):
    """
    DFS-based topological sort.

    Returns:
        Topological ordering (or empty list if cycle exists)
    """
    graph = defaultdict(list)
    for u, v in edges:
        graph[u].append(v)

    # 0 = unvisited, 1 = in current path, 2 = fully processed
    state = [0] * num_vertices
    result = []

    def dfs(vertex):
        if state[vertex] == 1:  # Back edge = cycle
            return False
        if state[vertex] == 2:  # Already processed
            return True

        state[vertex] = 1  # Mark as in progress

        for neighbor in graph[vertex]:
            if not dfs(neighbor):
                return False

        state[vertex] = 2  # Mark as fully processed
        result.append(vertex)  # Add after processing all dependencies
        return True

    for v in range(num_vertices):
        if state[v] == 0:
            if not dfs(v):
                return []  # Cycle detected

    return result[::-1]  # Reverse for correct order

When to Use Topological Sort

Use topological sort when:

  • Task dependencies must be satisfied (build systems, package managers)
  • Course prerequisites define a valid schedule
  • Event ordering matters (family tree generations, data processing pipelines)
  • You need to detect cycles in directed graphs

Don’t use topological sort when:

  • Graph is not a DAG (if you need to handle cycles, use other algorithms)
  • Undirected graph (topological order is undefined)

Trade-off Analysis

AspectKahn’s AlgorithmDFS-based
Time ComplexityO(V + E)O(V + E)
Space ComplexityO(V + E)O(V + E)
ApproachIterative (BFS)Recursive (DFS)
Cycle DetectionExplicit (remaining vertices)Via back-edge detection
Order TypeArbitrary valid orderReverse of finish times

Topological Sort Flow

graph TD
    A["Build graph & compute<br/>indegree for each vertex"] --> B{"Any vertex with indegree = 0?"}
    B -->|Yes| C["Pick vertex with<br/>indegree 0"]
    C --> D["Add to result<br/>Remove from graph"]
    D --> E["Reduce indegree of<br/>all outgoing neighbors"]
    E --> F{"Any neighbor's indegree = 0?"}
    F -->|Yes| C
    F -->|No| G{"All vertices processed?"}
    B -->|No| H["Cycle detected<br/>Return empty result"]
    G -->|No| B
    G -->|Yes| J["Return topological<br/>ordering"]

Production Failure Scenarios

  1. Cycle detection failures causing silent wrong results: If the graph contains a cycle and the implementation doesn’t check for one, vertices in the cycle will never reach indegree 0. Kahn’s algorithm will exit with fewer vertices than expected, but the code might not alert on this. Always verify that len(result) == num_vertices and raise or log an error when a cycle is detected.

  2. Kahn’s algorithm queue starvation: When many vertices have indegree > 0 but no queue is processed, the algorithm can appear to hang even though it will eventually terminate. In production with large graphs, monitor the queue size—if it empties before all vertices are processed, that means a cycle exists.

  3. DFS recursion stack overflow on deep graphs: For graphs with depth approaching Python’s recursion limit (~1000), the DFS-based approach can crash. Always use Kahn’s algorithm for graphs that might be deep chains, or implement DFS iteratively with an explicit stack.

  4. Multiple valid orders breaking deterministic builds: Kahn’s algorithm produces different orderings depending on which zero-indegree vertex is picked from the queue. If your build system relies on deterministic ordering for reproducible outputs, this non-determinism can cause different builds on different runs. Use a tie-breaking rule (e.g., always pick the smallest vertex ID) or sort the final result.

Course Schedule Problem

def can_finish_courses(num_courses, prerequisites):
    """
    Check if all courses can be finished given prerequisites.

    Args:
        num_courses: Total number of courses (0 to num_courses-1)
        prerequisites: List of [a, b] where a must be taken before b

    Returns:
        True if all courses can be completed
    """
    edges = [(b, a) for a, b in prerequisites]  # b -> a means b is prerequisite of a
    order = topological_sort_kahn(num_courses, edges)
    return len(order) == num_courses

Alien Dictionary Problem

Given a sorted dictionary of words from an alien language, determine the order of characters in that language. Each adjacent pair of words constrains the relative order of characters — the first differing character in word[i] must come before the corresponding character in word[i+1].

from collections import defaultdict, deque

def alien_order(words):
    """
    Determine the order of characters in an alien language
    from a sorted dictionary.

    Args:
        words: List of words sorted in alien lexicographical order

    Returns:
        String representing character order, or empty string if invalid
    """
    graph = defaultdict(set)
    indegree = defaultdict(int)

    # Collect all unique characters
    chars = set()
    for word in words:
        for ch in word:
            chars.add(ch)

    # Initialize indegree for all characters
    for ch in chars:
        indegree[ch] = 0

    # Build graph by comparing adjacent words
    for i in range(len(words) - 1):
        w1, w2 = words[i], words[i + 1]

        # Invalid case: second word is a prefix of first
        if len(w1) > len(w2) and w1[: len(w2)] == w2:
            return ""

        for ch1, ch2 in zip(w1, w2):
            if ch1 != ch2:
                if ch2 not in graph[ch1]:
                    graph[ch1].add(ch2)
                    indegree[ch2] += 1
                break

    # Kahn's algorithm
    queue = deque([ch for ch in chars if indegree[ch] == 0])
    result = []

    while queue:
        ch = queue.popleft()
        result.append(ch)
        for neighbor in graph[ch]:
            indegree[neighbor] -= 1
            if indegree[neighbor] == 0:
                queue.append(neighbor)

    if len(result) != len(chars):
        return ""  # Cycle detected

    return "".join(result)

This problem showcases how topological sort translates naturally to language processing: characters act as vertices, and ordering constraints from pairwise word comparisons form the edges. If the result is empty, the dictionary is invalid (inconsistent ordering or cycle).

Quick Recap Checklist

  • Topological sort only works on DAGs
  • Kahn’s algorithm: remove zero-indegree vertices iteratively
  • DFS-based: reverse the order of finished vertices
  • If topological order doesn’t include all vertices, a cycle exists
  • Both methods run in O(V + E) time

Observability Checklist

Track topological sort executions to catch cycle detection failures and performance anomalies.

Core Metrics

  • Queue size at each step (Kahn’s) — monitors how many zero-indegree vertices are pending
  • Vertices processed count vs. total vertices expected
  • DFS recursion depth for DFS-based variant
  • Time per indegree update operation
  • Cycle detection result (len(result) == num_vertices)

Health Signals

  • Queue empty before all vertices processed: cycle detected
  • Queue size growing unbounded: graph may have many parallel dependency chains
  • DFS recursion depth approaching stack limit (~1000 in Python)
  • Result length significantly less than vertex count: cycle in graph
  • Performance degrading on subsequent runs with same graph

Alerting Thresholds

  • Result count < num_vertices: cycle detected, alert immediately
  • Queue empty with vertices remaining: cycle exists, alert
  • DFS depth > 800: warn about stack overflow risk
  • Kahn’s queue size > V × 0.5 consistently: possible graph structure issue

Distributed Tracing

  • Trace topological sort with vertex count, edge count, and algorithm variant (Kahn/DFS)
  • Include queue depth over time as span metadata for Kahn’s algorithm
  • Track cycle detection result in span attributes
  • Correlate slow runs with high edge counts or deep dependency chains

Security Notes

Topological sort has specific security concerns when input graph is untrusted.

DAG poisoning with injected cycles

Topological sort only works on DAGs. A cycle in the input causes the algorithm to produce an incomplete ordering—the vertices in the cycle never reach indegree zero, so they never appear in the result. If your implementation doesn’t check for this, it returns a partial ordering that looks valid but silently omits the problematic vertices.

In a build system, a circular dependency injected by an attacker could prevent an entire software build from completing. The system waits indefinitely for a resolution that never comes, or the dependency resolver silently skips the cyclic module and proceeds with a broken build.

Mitigation checklist:

  • Always check that len(result) == num_vertices after running topological sort
  • If counts don’t match, reject the input entirely and alert on the cyclic dependency
  • Do not attempt to “salvage” partial results—the omitted vertices indicate broken dependencies
  • Log which vertices were missing to help identify the cycle in the input
  • For build systems, run cycle detection as a separate validation step before attempting resolution

Queue starvation as denial-of-service

Kahn’s algorithm processes vertices in order of indegree zero. A crafted DAG with very long chains (depth approaching V) forces the algorithm to process one vertex per iteration, consuming CPU with minimal progress per step. The algorithm is correct—it eventually finishes—but the runtime approaches O(VE) for certain chain-heavy structures.

When processing time per step is bounded, a chain-heavy DAG can cause repeated timeouts even though the algorithm would eventually succeed. An attacker can exploit this to make your service appear unresponsive.

Mitigation checklist:

  • Set maximum limits on graph depth (longest path length) and width (maximum parallel zero-indegree vertices)
  • Set a per-step time budget and abort if exceeded, treating it as a potential DoS attempt
  • Monitor average queue depth—if it stays near1 for extended periods, the graph may be chain-heavy
  • Consider using DFS-based topological sort for graphs that are expected to be deep chains (avoids queue overhead)
  • Log graph structure metrics (depth, width, iteration count) for post-hoc analysis of slow runs

Deterministic build violation via input ordering

Kahn’s algorithm produces different valid orderings depending on which zero-indegree vertex is picked when multiple exist. If an attacker controls the input order of edges or vertices, they can bias the algorithm toward a specific ordering that produces a particular build artifact—useful for injecting compromised binaries into a trusted build pipeline.

The attack requires no special privileges: just arrange the input so a specific valid ordering emerges. The resulting binary may be functionally correct but contain different optimization decisions, inlined values, or module ordering that benefits the attacker.

Mitigation checklist:

  • Apply a deterministic tie-breaking rule: always pick the smallest vertex ID (or lexicographically smallest label) among zero-indegree candidates
  • Log which ordering was selected for every build, including the tie-breaking decision
  • Verify build artifacts against a known-good hash if your build system supports reproducible builds
  • Consider using DFS-based topological sort which produces a more deterministic reverse-finish-time ordering
  • Monitor for unexpected changes in build artifact hashes that correlate with input ordering changes

Interview Questions

1. Why can't topological sort work on graphs with cycles?

Topological sort requires a linear ordering where every edge goes from earlier to later. In a cyclic graph, you can't pick a "first" vertex because every vertex has at least one incoming edge from within the cycle. Formally, a graph has a topological ordering if and only if it's a DAG. This equivalence also means topological sort is a valid cycle detection test.

2. Is the topological ordering unique?

No—in general, multiple valid topological orderings can exist. For example, if you have two independent tasks A and B with no dependencies between them, both [A, B] and [B, A] are valid. The number of possible orderings grows combinatorially with graph structure. Kahn's algorithm produces different results depending on which zero-indegree vertex you pick when multiple exist.

3. How do you find the shortest path in a DAG?

For shortest paths in a DAG, use topological sort as a preprocessing step: 1) Compute topological ordering, 2) Process vertices in that order, 3) Relax edges from each vertex. This achieves O(V + E) for single-source shortest paths in DAGs, even with negative edges (since there are no cycles). Topological sort eliminates the need for complex shortest path algorithms on acyclic graphs.

4. What's the difference between indegree and outdegree in topological sort?

Indegree = number of edges pointing INTO a vertex (dependencies). Outdegree = number of edges pointing OUT from a vertex (dependents). In Kahn's algorithm, we start with vertices that have indegree 0 (no dependencies) because these tasks have no prerequisites and can be executed immediately. As we process each vertex, we decrement the indegree of its neighbors since one of their dependencies has been satisfied.

5. What is the time and space complexity of Kahn's algorithm?

Both time and space complexity are O(V + E), where V is the number of vertices and E is the number of edges. Building the adjacency list takes O(V + E) space and O(E) time to populate. Processing each vertex once and each edge once gives O(V + E) time. The queue holds at most V vertices, so space is O(V) for the queue plus O(V + E) for the graph representation.

6. How do you detect a cycle using Kahn's algorithm?

After Kahn's algorithm finishes, compare the length of the result list with the total number of vertices. If len(result) != num_vertices, a cycle exists. The vertices in the cycle never reach indegree 0 because each vertex in the cycle has at least one incoming edge from within the cycle, so they are never added to the zero-indegree queue and remain unprocessed.

7. How can you make topological sort produce a deterministic ordering?

Kahn's algorithm produces non-deterministic orderings when multiple vertices have indegree 0 simultaneously. To make the output deterministic, use a priority queue (min-heap) instead of a simple FIFO queue. Always pick the vertex with the smallest ID (or lexicographically smallest label) among the zero-indegree candidates. This guarantees the same ordering every run, which is critical for reproducible builds and deterministic tests.

8. What real-world systems use topological sort?

Topological sort appears in many production systems:

  • Build systems (Make, Bazel, Gradle) — determine compilation order of interdependent source files
  • Package managers (apt, pip, npm) — resolve dependency installation order
  • Course scheduling — verify prerequisite chains and produce valid enrollment sequences
  • Database migration tools — order schema changes respecting foreign key dependencies
  • Data processing pipelines (Apache Spark, Airflow) — schedule DAG stages for execution
  • Spreadsheet computation — evaluate cell dependencies in the correct order
9. What happens if you run DFS-based topological sort on a graph with depth > 1000 in Python?

Python's default recursion limit is around 1000. If the DFS recursion depth exceeds this, Python raises a RecursionError. This is a common production failure for deep dependency chains. Mitigations include: (1) using Kahn's algorithm instead (iterative, no recursion), (2) implementing DFS iteratively with an explicit stack, or (3) increasing the recursion limit with sys.setrecursionlimit() (though this risks stack overflow crashes).

10. How does topological sort relate to strongly connected components?

A graph that is not a DAG contains at least one strongly connected component (SCC) of size > 1. If you condense a directed graph into its SCCs (using Kosaraju's or Tarjan's algorithm), the resulting condensation DAG always admits a topological ordering. In fact, the order in which SCCs are popped from the stack in Kosaraju's algorithm naturally gives a reverse topological order of the condensation graph. This connection is foundational for problems like 2-SAT satisfiability.

11. Can topological sort be parallelized?

Expected answer points:

  • Level-based parallelism is possible: vertices at same depth can be processed concurrently
  • In Kahn's algorithm, all zero-indegree vertices at each step can be processed in parallel
  • Barrier synchronization required after each level for indegree updates
  • Real-world implementations in Spark/Airflow exploit this by partitioning DAG stages at topological boundaries
12. How does Kahn's algorithm handle disconnected components?

Expected answer points:

  • Starts with ALL zero-indegree vertices in initial queue, naturally processing all components
  • Each disconnected component with no incoming edges will have vertices reach indegree 0
  • Result interleaves vertices from different components based on when zero-degree nodes appear
  • Post-processing needed if component order must be preserved
13. What is the difference between Kahn's algorithm and DFS for cycle detection?

Expected answer points:

  • Kahn's detects cycles implicitly at end: remaining vertices unprocessed indicate cycle
  • DFS detects cycles during traversal via back edges (vertex in current recursion stack)
  • DFS pinpoints which vertices are in cycle; Kahn's only says SOME cycle exists
  • For cycle detection alone, Kosaraju's or Tarjan's algorithm is more direct
14. Why does DFS-based topological sort reverse the result?

Expected answer points:

  • DFS records vertices in finish order (when all dependencies are processed)
  • Descendants are added before ancestors, producing reverse of desired order
  • Example: A → B → C, DFS finishes C first, then B, then A = [C, B, A]
  • Reversing gives [A, B, C], the correct topological order
15. How does using a priority queue instead of a FIFO queue change the output?

Expected answer points:

  • Produces deterministic lexicographically smallest valid topological ordering
  • Eliminates non-determinism when multiple zero-indegree vertices exist
  • Essential for reproducible builds and deterministic tests
  • Time complexity becomes O((V + E) log V) due to heap operations
16. Can you find all valid topological orderings of a DAG?

Expected answer points:

  • Yes, via backtracking with pruning
  • Count grows super-exponentially (Bell numbers) - only feasible for very small DAGs
  • Pruning: remaining vertices cannot form valid order
  • Check uniqueness by seeing if any step has more than one zero-indegree choice
17. How does the choice of algorithm affect space complexity for large graphs?

Expected answer points:

  • Both use O(V + E) for graph storage
  • Kahn's: O(V) queue + O(V) indegree = O(V) auxiliary
  • DFS: O(V) state + O(V) result + O(V) recursion stack = O(V) auxiliary
  • External-memory algorithms needed for graphs not fitting in memory
18. How do you handle dynamic graph updates in topological sort?

Expected answer points:

  • Full recomputation often needed after edge insertions
  • For additions only: new edges only affect downstream vertices
  • If both endpoints already in result, order doesn't change
  • Fully dynamic (mixed insertions/deletions) requires sophisticated data structures or recomputation
19. Why does topological sort not apply to undirected graphs?

Expected answer points:

  • Topological ordering requires directed edges for partial order (asymmetric, transitive)
  • Undirected edges are symmetric - no notion of one vertex being "before" another
  • Any connected undirected graph has at least one cycle, violating DAG requirement
  • For undirected graphs, use connected components or spanning tree algorithms instead
20. What is the minimum number of edges in a DAG with V vertices?

Expected answer points:

  • Minimum: V - 1 edges (assuming weakly connected, simple chain)
  • Example: A₁ → A₂ → ... → Aᵥ is valid DAG with exactly V - 1 edges
  • Cannot have fewer edges and still connect V vertices
  • Maximum: V×(V-1)/2 (all edges in one direction, tournament DAG)

Explore these related topics to deepen your understanding:

Conclusion

Topological sort handles dependency ordering in DAGs two ways: Kahn’s algorithm (BFS, iteratively removes zero-indegree vertices) or DFS-based (records vertices in reverse finish order). Both run in O(V + E) time. One practical gotcha: if your final ordering is missing vertices, a cycle exists in the graph. For graph representation fundamentals, see Graphs: Adjacency List and Matrix, or Depth-Limited Search for related traversal patterns.

Category

Related Posts

Bellman-Ford Algorithm: Shortest Paths with Negative Weights

Learn the Bellman-Ford algorithm for single-source shortest paths including negative edge weights and negative cycle detection.

#bellman-ford #shortest-path #graph-algorithms

Dijkstra's Algorithm: Finding Shortest Paths in Weighted Graphs

Master Dijkstra's algorithm for single-source shortest path problems in weighted graphs with positive edges, including implementations and trade-offs.

#dijkstra #shortest-path #graph-algorithms

Floyd-Warshall Algorithm: All-Pairs Shortest Paths

Master the Floyd-Warshall algorithm for finding shortest paths between all pairs of vertices in weighted graphs, with implementation examples and trade-offs.

#floyd-warshall #shortest-path #all-pairs