Topological Sort Algorithm: Working, Explanation, and Dependency Ordering

topological sort algorithm and working of this algorithm

Whenever I’ve had to figure out a valid order to complete a set of tasks with dependencies — like which courses to take first, or in what sequence to compile files that depend on each other — I’ve relied on topological sorting. It’s the algorithm that takes a directed acyclic graph and arranges its nodes into a linear order such that every edge points from an earlier node to a later one. What I like about it is that it doesn’t just solve an abstract graph problem; it directly models scheduling and dependency-resolution problems that show up constantly in real software systems, from build tools to package managers.

History and Background

I trace the idea of topological ordering to foundational work in graph theory from the early-to-mid 20th century, with formal treatment appearing in the work of mathematicians studying partially ordered sets (posets) and directed acyclic graphs. The specific linear-time algorithm using depth-first search is generally attributed to Robert Tarjan, who described it in the early 1970s as part of his broader body of work on graph algorithms, including strongly connected components. The alternative approach using in-degree counting is commonly known as Kahn’s algorithm, published by Arthur B. Kahn in 1962 in a paper about topological sorting of large networks, and it remains the more intuitive of the two approaches for many people, myself included, when first learning the concept.

Problem Statement

I have a directed graph representing tasks and their dependencies, where an edge from node u to node v means task u must be completed before task v. I need to produce a linear ordering of all nodes such that for every directed edge u -> v, u appears before v in the ordering. This is only possible if the graph is a Directed Acyclic Graph (DAG) — if there’s a cycle, no valid ordering can exist, since it would require some task to happen before itself. Topological sort must therefore both produce a valid ordering when possible and detect the impossibility (a cycle) when it isn’t.

Core Concepts

  • Directed Acyclic Graph (DAG): a directed graph containing no cycles; topological sort is only defined for such graphs.
  • In-degree: the number of incoming edges to a node, representing the number of unmet prerequisites.
  • Source node: a node with in-degree zero, meaning it has no unfulfilled dependencies and can be scheduled immediately.
  • Kahn’s algorithm: an approach using in-degree counting and a queue of ready (zero in-degree) nodes.
  • DFS-based approach: an approach that performs a depth-first search and prepends each node to the result list once all of its descendants have been fully explored (post-order).

How It Works

I describe both standard approaches, since each has its own intuitive appeal:

Kahn’s algorithm (BFS-based):

  1. I compute the in-degree of every node.
  2. I initialize a queue with all nodes that currently have in-degree zero.
  3. I repeatedly dequeue a node, append it to the result list, and decrement the in-degree of each of its neighbors.
  4. If any neighbor’s in-degree drops to zero, I enqueue it.
  5. If I process all nodes, the result list is a valid topological order; if not all nodes are processed, the graph has a cycle.

DFS-based algorithm:

  1. I perform a standard depth-first search over all nodes.
  2. When the DFS finishes fully exploring a node (i.e., all its descendants have been visited), I push that node onto a stack (or prepend it to a result list).
  3. After the DFS completes for the entire graph, I reverse the stack (or read the result list in reverse) to get the topological order.
  4. I detect cycles by tracking nodes currently in the recursion stack (the “gray” state) — if I revisit a gray node, a cycle exists.

Working Principle

Both approaches rely on the same underlying invariant: a node can only be placed in the ordering once all of its prerequisites have already been placed. Kahn’s algorithm enforces this directly and greedily — a node is only ever added to the queue (and thus eventually to the output) once every one of its incoming edges has been “consumed” by processing its predecessors. The DFS-based approach enforces the same invariant indirectly, through the structure of post-order traversal: a node is only finished (and thus added to the result) after all paths leading out of it, including through its dependents, have been fully explored, which guarantees that anything reachable from it — meaning anything depending on it in the reversed sense — is accounted for before it takes its final position when the list is reversed.

Mathematical Foundation

A topological order exists for a directed graph G = (V, E) if and only if G is acyclic. This is formalized by the following:

$$ \text{Topological order exists} \iff G \text{ contains no directed cycle} $$

For Kahn’s algorithm, if I let S_0 be the set of nodes with in-degree 0, and repeatedly define S_{i+1} as the set of nodes whose in-degree becomes 0 after removing S_i and its outgoing edges, then a topological order exists if and only if:

$$ \bigcup_{i \geq 0} S_i = V $$

meaning every node is eventually reached by this process. If the union falls short of V, the remaining nodes form one or more cycles.

The total complexity of processing every node and every edge exactly once (in either approach) is:

$$ T(|V|, |E|) = O(|V| + |E|) $$

Diagrams

flowchart TD
    A[Compute in-degree for every node] --> B[Enqueue all nodes with in-degree 0]
    B --> C{Queue empty?}
    C -- Yes --> H{All nodes processed?}
    H -- Yes --> I[Valid topological order found]
    H -- No --> J[Cycle detected]
    C -- No --> D[Dequeue node u, append to result]
    D --> E[For each neighbor v of u: decrement in-degree of v]
    E --> F{in-degree of v == 0?}
    F -- Yes --> G[Enqueue v]
    G --> C
    F -- No --> C

Pseudocode

Kahn’s Algorithm:

function topologicalSortKahn(graph):
    inDegree = map of node -> 0 for all nodes
    for each node u in graph:
        for each neighbor v of u:
            inDegree[v] += 1

    queue = all nodes with inDegree == 0
    result = []

    while queue is not empty:
        u = queue.dequeue()
        result.append(u)
        for each neighbor v of u:
            inDegree[v] -= 1
            if inDegree[v] == 0:
                queue.enqueue(v)

    if length(result) != number of nodes in graph:
        return "Cycle detected, no valid topological order"
    return result

DFS-based Algorithm:

function topologicalSortDFS(graph):
    visited = set()
    inStack = set()
    result = []
    hasCycle = false

    function dfs(u):
        visited.add(u)
        inStack.add(u)
        for each neighbor v of u:
            if v in inStack:
                hasCycle = true
                return
            if v not in visited:
                dfs(v)
        inStack.remove(u)
        result.prepend(u)   // or push to stack, then reverse later

    for each node u in graph:
        if u not in visited:
            dfs(u)

    if hasCycle:
        return "Cycle detected, no valid topological order"
    return result

Step-by-Step Example

Consider a small dependency graph for getting dressed:

  • Underwear -> Pants
  • Pants -> Shoes
  • Pants -> Jacket
  • Shirt -> Jacket
  • Socks -> Shoes

Using Kahn’s algorithm:

In-degrees: Underwear = 0, Shirt = 0, Socks = 0, Pants = 1, Jacket = 2, Shoes = 2.

I start with Underwear, Shirt, Socks in the queue (order among equals can vary). I process Underwear, decrementing Pants to 0, so I enqueue Pants. I process Shirt, decrementing Jacket to 1. I process Socks, decrementing Shoes to 1. I process Pants, decrementing Jacket to 0 (enqueue Jacket) and Shoes to 0 (enqueue Shoes). Finally I process Jacket and Shoes.

One valid result: Underwear, Shirt, Socks, Pants, Jacket, Shoes — every dependency is respected.

Time Complexity

  • Best case: O(|V| + |E|) — both algorithms always process every node and edge exactly once, so there’s no better-case shortcut.
  • Average case: O(|V| + |E|).
  • Worst case: O(|V| + |E|) — this bound holds regardless of graph shape, since the work is strictly linear in the size of the graph representation (using an adjacency list).

Space Complexity

Both approaches require O(|V|) space for auxiliary structures: the in-degree array and queue in Kahn’s algorithm, or the visited/in-stack sets and recursion stack in the DFS-based approach. The adjacency list representation of the graph itself takes O(|V| + |E|) space, and the output ordering takes O(|V|) space.

Correctness Analysis

For Kahn’s algorithm, I argue correctness by induction on the processing order: a node is only ever enqueued once its in-degree reaches zero, which by construction means every predecessor of that node (according to the current graph state) has already been dequeued and placed into the result. This guarantees that at the moment any node is added to the result, all of its prerequisites already appear earlier, satisfying the topological order property. If the graph has a cycle, no node within that cycle can ever reach in-degree zero (since each depends circularly on another within the cycle), so the algorithm correctly fails to include all nodes, signaling a cycle.

For the DFS-based approach, I rely on the classic property of DFS finish times in a DAG: for any edge u -> v, the finish time of u is always later than the finish time of v, because v must be fully explored (finished) before the recursive call on u can return. Reversing the finish-time order therefore guarantees u appears before v, satisfying the topological order property for every edge.

Advantages

  • Both approaches run in optimal linear time relative to graph size.
  • Kahn’s algorithm naturally detects cycles as a side effect, without extra bookkeeping.
  • The DFS-based approach integrates naturally with other DFS-based graph algorithms, such as strongly connected component detection.
  • Produces orderings directly usable for real scheduling problems: build systems, task schedulers, and course prerequisite planning.

Disadvantages

  • Only applicable to Directed Acyclic Graphs; any cycle makes the problem unsolvable, and detecting this adds a small amount of extra logic.
  • The resulting order is generally not unique — multiple valid topological orders can exist for the same graph, which can be undesirable when a specific, deterministic order is required (this can be fixed by using a priority queue instead of a plain queue in Kahn’s algorithm, at extra cost).
  • Doesn’t account for weighted priorities among independent tasks by default; extra criteria must be added if such preferences matter.
  • The DFS-based version requires careful implementation of the “gray/black” node-state tracking to correctly detect cycles, which is a common source of subtle bugs.

Applications

  • Build systems (like Make, Bazel, or MSBuild) determining the correct order to compile files based on dependency graphs.
  • Course scheduling systems, ordering classes based on prerequisite requirements.
  • Package managers (npm, pip, apt) resolving installation order based on dependency declarations.
  • Task scheduling in project management tools, where certain tasks must precede others.
  • Spreadsheet formula evaluation order, where cells depending on other cells must be recalculated in the correct sequence.

Implementation in C

#include <stdio.h>
#include <stdlib.h>

#define MAX_NODES 100

int adj[MAX_NODES][MAX_NODES]; /* adjacency matrix */
int inDegree[MAX_NODES];
int numNodes;

void topologicalSortKahn() {
    int queue[MAX_NODES];
    int front = 0, back = 0;
    int result[MAX_NODES];
    int resultCount = 0;

    int localInDegree[MAX_NODES];
    for (int i = 0; i < numNodes; i++) {
        localInDegree[i] = inDegree[i];
        if (localInDegree[i] == 0) {
            queue[back++] = i; /* enqueue nodes with no prerequisites */
        }
    }

    while (front < back) {
        int u = queue[front++];
        result[resultCount++] = u;

        for (int v = 0; v < numNodes; v++) {
            if (adj[u][v]) {
                localInDegree[v]--;
                if (localInDegree[v] == 0) {
                    queue[back++] = v;
                }
            }
        }
    }

    if (resultCount != numNodes) {
        printf("Cycle detected! No valid topological order exists.\n");
        return;
    }

    printf("Topological order: ");
    for (int i = 0; i < resultCount; i++) {
        printf("%d ", result[i]);
    }
    printf("\n");
}

int main() {
    numNodes = 6; /* 0=Underwear,1=Pants,2=Shoes,3=Jacket,4=Shirt,5=Socks */

    for (int i = 0; i < MAX_NODES; i++) {
        inDegree[i] = 0;
        for (int j = 0; j < MAX_NODES; j++) adj[i][j] = 0;
    }

    /* Add edges: u -> v means u must come before v */
    int edges[][2] = {{0,1}, {1,2}, {1,3}, {4,3}, {5,2}};
    int numEdges = 5;

    for (int i = 0; i < numEdges; i++) {
        int u = edges[i][0], v = edges[i][1];
        adj[u][v] = 1;
        inDegree[v]++;
    }

    topologicalSortKahn();
    return 0;
}

Sample Input and Output

Input: Nodes 0..5 representing Underwear, Pants, Shoes, Jacket, Shirt, Socks, with edges Underwear->Pants, Pants->Shoes, Pants->Jacket, Shirt->Jacket, Socks->Shoes.

Output:

Topological order: 0 4 5 1 3 2

(Underwear, Shirt, Socks, Pants, Jacket, Shoes — order among independent nodes may vary by implementation)

Optimization Techniques

  • Adjacency list over adjacency matrix: for sparse graphs (which most dependency graphs are), using adjacency lists instead of matrices reduces time complexity from O(V^2) to O(V + E) and saves memory.
  • Deterministic ordering with a priority queue: replacing the plain queue in Kahn’s algorithm with a min-heap ensures a consistent, smallest-ID-first ordering when multiple nodes are simultaneously ready, useful for reproducible build outputs.
  • Iterative DFS: converting the recursive DFS-based approach into an iterative one using an explicit stack avoids recursion-depth issues on very large or deeply chained graphs.
  • Parallel scheduling: since all nodes within the same “layer” (same Kahn’s algorithm iteration) are mutually independent, they can be processed in parallel, which is exactly how many real build systems achieve concurrency.

Common Mistakes

  • Forgetting to check whether the result includes all nodes, which is the standard way to detect a cycle in Kahn’s algorithm.
  • Confusing the direction of an edge relative to the ordering — an edge u -> v means u comes first, not v.
  • In the DFS-based approach, forgetting to track the “currently in recursion stack” state separately from “already fully visited,” which is required to correctly detect back edges (cycles) versus cross/forward edges.
  • Assuming a unique topological order exists; many graphs have multiple valid orderings, and code that depends on a specific one without enforcing it (e.g., via a priority queue) can behave unpredictably.

Further Reading

  • Kahn, A. B. “Topological sorting of large networks,” Communications of the ACM, 1962: https://dl.acm.org/doi/10.1145/368996.369025
  • Cormen, T. H., Leiserson, C. E., Rivest, R. L., Stein, C. “Introduction to Algorithms” (CLRS), Chapter on Elementary Graph Algorithms: https://mitpress.mit.edu/9780262046305/introduction-to-algorithms/
  • GeeksforGeeks, “Topological Sorting”: https://www.geeksforgeeks.org/dsa/topological-sorting/
  • Wikipedia, “Topological sorting”: https://en.wikipedia.org/wiki/Topological_sorting
Total
0
Shares

Leave a Reply

Previous Post
floyd warshall algorithm and working of this algorithm

Floyd-Warshall Algorithm: Working, Explanation, and All-Pairs Shortest Paths

Next Post
flood fill algorithm and working of this algorithm

Flood Fill Algorithm: Working, Explanation, and Image Processing Applications

Related Posts