Skip to main content

Topological Sort Algorithm Implementation and Applications

Complete Java solution for topological sorting with DFS and BFS approaches, including cycle detection and practical use cases.

Topological sorting is a linear ordering of vertices in a directed acyclic graph (DAG) where for every directed edge u → v, vertex u comes before v in the ordering. This fundamental graph algorithm has wide applications in task scheduling, dependency resolution, and course prerequisite planning.

1. Pattern Restatement

Clarifying questions to ask the interviewer:

  1. Are there any constraints on the number of nodes or edges in the graph?
  2. Can the input be mutated (e.g., can we modify the input adjacency list or graph structure)?
  3. Should we return one valid topological ordering or all possible orderings?
  4. Are duplicate edges or self-loops allowed in the input?
  5. What is the expected time complexity for follow-up — O(V + E) is optimal, but can we do better?

Restatement of the pattern:

Topological sorting is a linear ordering of vertices in a directed acyclic graph (DAG) such that for every directed edge u → v, vertex u comes before v in the ordering. It's used to model dependencies and precedence relationships.

Signal words and constraint shape:

  • "Before" or "after" constraints (e.g., task A must complete before B)
  • Dependency chains or precedence graphs
  • "Build order" or "schedule" problems
  • Directed acyclic graph (DAG) structure
  • "No cycles allowed" or "must be consistent"

Assumptions:

  • The input represents a valid DAG (no cycles)
  • All nodes in the dependency graph are reachable from some starting point
  • The topological order is not unique (multiple valid orders may exist)
  • We're working with a directed graph where edges represent dependencies

Scenario suitability:

This pattern is best suited for static scenarios where the full dependency graph is known upfront. It's not ideal for streaming or online problems where new dependencies arrive dynamically, as maintaining the topological order would require recomputing or more complex data structures.

The pattern works well when we need to process items in dependency order, such as task scheduling, course prerequisites, or build systems.

2. When to Use This Pattern

Signal words and constraint shapes in a problem statement that indicate this pattern:

  • "Ordering constraints" or "dependencies between tasks"
  • "Build order" or "prerequisites"
  • "No circular dependencies allowed"
  • "Topological ordering" or "topological sort"
  • "Course schedule" or "task scheduling"
  • "In-degree" or "out-degree" in graph problems

Why this pattern fits better than the most common alternatives:

Topological sort is specifically designed for directed acyclic graphs (DAGs) where we need to arrange nodes in a linear order respecting dependencies. Unlike BFS/DFS which explore all paths, or greedy approaches which make local optimal choices, topological sort systematically processes nodes with zero in-degree to ensure all prerequisites are satisfied before processing dependent tasks. It's more efficient than trying to detect cycles with DFS and then attempting to order nodes.

  • Cycle detection: If the problem only asks whether a valid order exists (not what that order is), we can detect cycles using DFS or BFS without full topological ordering
  • Kahn's algorithm variant: When we have a specific starting point or need to process nodes in batches, we might use modified topological approaches
  • Strongly connected components: When we need to group nodes into equivalence classes based on mutual reachability
  • DFS-based topological sort: When we're comfortable with recursion stack and want to avoid maintaining in-degree arrays

The core insight that makes this pattern work:

The key insight is that if a valid ordering exists, there must be at least one node with in-degree zero (no prerequisites). By repeatedly removing such nodes and updating the in-degrees of their neighbors, we can systematically build a valid sequence. If any remaining nodes have in-degrees > 0 at the end, we have a cycle and no valid ordering exists. This greedy approach works because we're always processing the "freest" nodes first, ensuring dependencies are satisfied in order.

3. Approach Evolution

Interview communication script:

"My first instinct is to think about this as a dependency resolution problem where we need to order tasks such that all prerequisites come before the task that depends on them. This immediately signals a topological sorting problem because we're dealing with directed edges representing dependencies.

But let me think about the brute force approach first - I could try all possible orderings and check if they satisfy dependencies, but that's factorial time O(n!) which is clearly not acceptable. That's O(n²) just to enumerate all permutations, and then checking validity adds another factor.

Let me think about how to optimize this. I notice that if I use a graph-based approach with in-degrees, I can systematically process nodes with zero incoming edges. This is the key insight - if a node has in-degree 0, it means no prerequisites remain unprocessed, so it's safe to process it.

I think the optimal approach is Kahn's algorithm using BFS with in-degrees. I maintain a queue of nodes with zero in-degree, process them one by one, and decrement the in-degrees of their neighbors. When any neighbor's in-degree becomes zero, I add it to the queue. This way I'm always processing nodes that are ready to be processed.

The key invariant here is that at any point, all nodes in the queue have in-degree 0, meaning all their prerequisites have been processed. If I end up with fewer nodes in my result than the total number of nodes, there's a cycle - that's how I detect invalid dependencies.

Let me walk through this more carefully to make sure I'm not missing anything..."

Brute-force approach and why it is not ideal:

The brute force approach would involve generating all possible permutations of the nodes and checking if each permutation satisfies the topological ordering constraints. For n nodes, there are n! permutations to check, and for each permutation, we need O(n) time to validate the ordering. This results in O(n! × n) time complexity, which is completely infeasible for any reasonable input size.

Improved approach and the key optimization:

The key optimization is to leverage the property that nodes with zero in-degree can be safely processed immediately. This allows us to use a BFS-like approach where we maintain a queue of nodes ready to be processed. When we process a node, we reduce the in-degrees of its neighbors. If any neighbor's in-degree becomes zero, it means all its prerequisites are now satisfied and we can add it to our processing queue.

Final approach with step-by-step reasoning:

  1. Build a graph representation and calculate in-degrees for all nodes
  2. Initialize a queue with all nodes having in-degree 0 (no prerequisites)
  3. Process nodes from the queue one by one:
    • Add the node to the topological order
    • For each neighbor, decrement its in-degree by 1
    • If any neighbor's in-degree becomes 0, add it to the queue
  4. If we processed all nodes, return the order; otherwise there's a cycle

The critical insight is that we only process nodes when all their dependencies are resolved, ensuring the topological order constraint.

Important invariants that must hold:

  1. Nodes in the queue always have in-degree 0 (no unprocessed prerequisites)
  2. When we process a node, all its dependencies have already been processed
  3. If there's a cycle, we will never be able to process all nodes (queue will eventually become empty before processing all nodes)
  4. The final result contains exactly n nodes if and only if there's no cycle

4. Data Structure Choice

For topological sorting, I'll use a combination of data structures to efficiently track dependencies and process nodes in the correct order:

Primary Data Structures:

  1. Adjacency List (HashMap<Integer, List>) - This represents the directed graph where each key is a node and its value is a list of nodes it points to. This allows for efficient O(1) neighbor lookups and handles sparse graphs well.

  2. In-degree Array (int[]) or HashMap<Integer, Integer> - This tracks the number of incoming edges for each node. It's essential for identifying nodes with no prerequisites (in-degree = 0) that can be processed first.

  3. Queue (LinkedList) - Used to process nodes in BFS order, maintaining the topological sequence. A queue ensures we process nodes with zero in-degrees in FIFO order.

Why These Choices Work:

The adjacency list efficiently represents the graph structure with O(V + E) space complexity where V is vertices and E is edges. It allows us to quickly find all neighbors of any node.

The in-degree array enables O(1) updates when we process nodes and efficiently identify new zero-in-degree nodes. This is crucial for the Kahn's algorithm implementation.

The queue maintains the processing order and naturally handles the BFS traversal that builds the topological ordering.

Space and Time Trade-offs:

Using an adjacency list instead of adjacency matrix saves space for sparse graphs (O(V + E) vs O(V²)). The in-degree array provides constant-time updates rather than requiring a full graph traversal to compute in-degrees.

Helper Structure:

A simple LinkedList for the queue provides O(1) add/remove operations at both ends, which is perfect for BFS processing. We could also use ArrayDeque but the standard LinkedList is more readable and sufficient for this use case.

These structures together enable an efficient O(V + E) solution that's clean, maintainable, and scales well with input size.

5. Algorithm Walkthrough

Let me trace through the topological sort algorithm using a concrete example to show how it works step by step.

Example: prerequisites = [[1,0],[2,0],[3,1],[3,2]] representing:

  • Course 1 requires course 0
  • Course 2 requires course 0
  • Course 3 requires course 1 AND course 2

Step-by-step execution:

  1. Build the graph and calculate in-degrees:

    • Graph: 0 → [1,2], 1 → [3], 2 → [3]
    • In-degrees: [0,1,1,2] (course 0 has 0 prerequisites, course 1 needs 1, etc.)
  2. Initialize queue with courses having 0 prerequisites:

    • Queue: [0] (course 0 has no prerequisites)
  3. Process course 0:

    • Remove course 0 from queue
    • Add course 0 to result list: [0]
    • For each dependent course (1 and 2): decrement in-degree
    • In-degrees now: [0,0,0,2] → course 1 and 2 now have 0 prerequisites each
    • Add courses 1 and 2 to queue: [1,2]
  4. Process course 1:

    • Remove course 1 from queue
    • Add course 1 to result list: [0,1]
    • For dependent course 3: decrement in-degree from 2 to 1
    • In-degrees now: [0,0,0,1]
  5. Process course 2:

    • Remove course 2 from queue
    • Add course 2 to result list: [0,1,2]
    • For dependent course 3: decrement in-degree from 1 to 0
    • In-degrees now: [0,0,0,0]
    • Add course 3 to queue: [3]
  6. Process course 3:

    • Remove course 3 from queue
    • Add course 3 to result list: [0,1,2,3]
    • No more dependents
  7. Final check:

    • Result size (4) equals number of courses (4)
    • Return [0,1,2,3]

Edge case handling:

  • If we had a cycle (e.g., 1→2 and 2→1), course 3 would never be added to queue because its in-degree would never reach 0
  • Algorithm would detect this by returning a result list shorter than total courses

The algorithm terminates correctly because it processes all nodes with zero in-degree, ensuring that we only process courses when all their prerequisites are satisfied.

6. Java Solution

import java.util.*;

public class TopologicalSort {
    
    public List<Integer> topologicalSort(int numCourses, int[][] prerequisites) {
        // Build adjacency list and in-degree array
        List<List<Integer>> graph = new ArrayList<>();
        int[] inDegree = new int[numCourses];
        
        // Initialize graph
        for (int i = 0; i < numCourses; i++) {
            graph.add(new ArrayList<>());
        }
        
        // Build graph and in-degree array
        for (int[] prereq : prerequisites) {
            int course = prereq[0];
            int prerequisite = prereq[1];
            
            graph.get(prerequisite).add(course);
            inDegree[course]++;
        }
        
        // Find all nodes with in-degree 0
        Queue<Integer> queue = new LinkedList<>();
        for (int i = 0; i < numCourses; i++) {
            if (inDegree[i] == 0) {
                queue.offer(i);
            }
        }
        
        List<Integer> result = new ArrayList<>();
        
        // Process nodes in topological order
        while (!queue.isEmpty()) {
            int current = queue.poll();
            result.add(current);
            
            // Reduce in-degree of neighbors
            for (int neighbor : graph.get(current)) {
                inDegree[neighbor]--;
                if (inDegree[neighbor] == 0) {
                    queue.offer(neighbor);
                }
            }
        }
        
        // Check if all courses can be completed
        return result.size() == numCourses ? result : new ArrayList<>();
    }
}

This solution implements Kahn's algorithm for topological sorting. It builds a directed graph from prerequisites, tracks in-degrees of each course, and processes nodes with zero in-degree to maintain the topological order. The algorithm returns a valid ordering if possible, or an empty list if there's a cycle (implying courses cannot be completed).

7. Complexity Analysis

Time Complexity: O(V + E) where V is the number of courses (vertices) and E is the number of prerequisites (edges).

The algorithm performs two main operations:

  1. Building the adjacency list and in-degree array: O(E) - we process each prerequisite once
  2. Processing nodes with zero in-degree: O(V + E) - each vertex is added to the queue at most once, and each edge is examined at most once

Since we process each edge exactly once during the BFS traversal, and each vertex is processed at most once, the overall time complexity is O(V + E).

Space Complexity: O(V + E)

This includes:

  • The adjacency list representation: O(E) - storing all prerequisite relationships
  • The in-degree array: O(V) - tracking incoming edges for each course
  • The queue for BFS traversal: O(V) - in worst case, all vertices have in-degree 0
  • The result list: O(V) - storing the topological ordering

The space complexity is dominated by the adjacency list and arrays, making it O(V + E).

Why these bounds are correct:

The algorithm is optimal for topological sorting because:

  1. We must examine each prerequisite relationship at least once to determine dependencies
  2. We must process each course at least once to build the ordering
  3. No algorithm can do better than O(V + E) for this problem since we need to read all input data

This complexity matches the theoretical lower bound for topological sorting, making it near-optimal. The solution is optimal because it makes exactly one pass through the graph structure, and each operation (building adjacency list, computing in-degrees, BFS traversal) is linear in the input size.

The solution is optimal for both time and space because:

  • It uses the standard BFS-based approach with in-degree tracking
  • It avoids redundant operations like reprocessing nodes or recalculating dependencies
  • It leverages the mathematical property that a valid topological sort exists if and only if the graph is acyclic

8. Edge Cases & Pitfalls

Empty and Minimal Inputs

  • Empty graph: An empty input array should return an empty result list. The algorithm must handle this case gracefully.
  • Single node: A single node with no dependencies should return that node as the only element in the result.
  • Self-loop: A single node with a self-dependency (e.g., 1 -> 1) creates a cycle and should return an empty list or throw an exception.

Duplicate Values and Repeated States

  • Duplicate edges: Multiple edges between the same pair of nodes should not affect the topological ordering, as we only care about the dependency relationships.
  • Duplicate nodes in input: If a node appears multiple times in the input, it should only be processed once.

Boundary Conditions

  • All nodes have zero in-degrees: When all nodes are independent, any order is valid for the topological sort.
  • All nodes have non-zero in-degrees: When every node depends on others, the graph must be acyclic for a valid result.
  • Large input size: With up to 20,000 nodes, the solution must be efficient and not cause stack overflow or memory issues.

Common Implementation Mistakes

  • Incorrect in-degree calculation: Failing to properly initialize or update in-degrees for all nodes can lead to incorrect results.
  • Not handling cycles: The algorithm must detect and reject cycles, otherwise it will produce invalid results or loop infinitely.
  • Modifying input data: Mutating the input array or graph structure during processing can lead to unexpected behavior and incorrect results.
  • Incorrect queue usage: Using a stack instead of a queue (or vice versa) can change the order of processing and produce incorrect topological orders.
  • Missing nodes: Not including all nodes in the result list if they have zero in-degrees can lead to incomplete topological orderings.

Additional Considerations

  • Unreachable nodes: Nodes that are not reachable from any starting point should still be included in the result if they have zero in-degrees.
  • Incorrect result size: The result list should contain exactly numCourses elements if a valid topological sort exists; otherwise, it should be empty or invalid.

9. Alternative Approaches

There are several alternative approaches to solving topological sorting problems, each with different trade-offs:

DFS-based approach: Instead of using in-degree tracking, we can use depth-first search to detect cycles and build the ordering. This approach traverses the graph using DFS and adds nodes to the result in reverse post-order. The key insight is that when we finish processing all neighbors of a node, we can safely add it to our result. This approach is more intuitive for some but requires careful handling of the recursion stack and cycle detection.

Kahn's algorithm with priority queue: When we need to impose additional ordering constraints (like lexicographically smallest valid topological order), we can modify Kahn's algorithm to use a priority queue (min-heap) instead of a regular queue. This ensures that among nodes with zero in-degree, we process the smallest one first, giving us the lexicographically smallest valid topological ordering.

Cycle detection with DFS: Rather than building the full topological order, if we only need to determine whether a valid ordering exists, we can use DFS to detect cycles. This is more efficient in terms of space complexity since we don't need to maintain the full ordering, but it's less useful when we actually need the ordering itself.

The chosen solution (Kahn's algorithm with in-degree tracking) is preferred because it's clean, efficient, and directly produces the desired topological ordering with clear invariants. The alternatives are useful when we have additional constraints or need to optimize for specific aspects like lexicographical ordering or cycle detection without full ordering.

10. Test Cases

import java.util.*;

public class TopologicalSortTestCases {
    
    // Test case 1: Basic valid DAG
    public void testBasicDAG() {
        // Input: [[1,0],[2,0],[3,1],[3,2]]
        // Expected: [0,1,2,3] or [0,2,1,3]
        int[][] edges = {{1,0},{2,0},{3,1},{3,2}};
        List<Integer> result = topologicalSort(edges, 4);
        // Verify that result is a valid topological ordering
        assert isValidTopologicalOrder(result, edges);
    }
    
    // Test case 2: Single node
    public void testSingleNode() {
        int[][] edges = {};
        List<Integer> result = topologicalSort(edges, 1);
        assert result.equals(Arrays.asList(0));
    }
    
    // Test case 3: Cycle detection
    public void testCycleDetection() {
        int[][] edges = {{1,0},{0,1}}; // Cycle: 0->1->0
        List<Integer> result = topologicalSort(edges, 2);
        assert result.isEmpty(); // Should return empty list for cycle
    }
    
    // Test case 4: Multiple valid orderings
    public void testMultipleValidOrderings() {
        int[][] edges = {{1,0},{2,0},{0,3}};
        List<Integer> result = topologicalSort(edges, 4);
        // Valid orderings: [3,0,1,2] or [3,0,2,1]
        assert isValidTopologicalOrder(result, edges);
    }
    
    // Test case 5: Empty graph
    public void testEmptyGraph() {
        int[][] edges = {};
        List<Integer> result = topologicalSort(edges, 5);
        // Any permutation of [0,1,2,3,4] is valid
        assert result.size() == 5;
        assert new HashSet<>(result).equals(new HashSet<>(Arrays.asList(0,1,2,3,4)));
    }
    
    // Helper method to validate topological ordering
    private boolean isValidTopologicalOrder(List<Integer> order, int[][] edges) {
        if (order.isEmpty()) return true;
        
        Map<Integer, Integer> position = new HashMap<>();
        for (int i = 0; i < order.size(); i++) {
            position.put(order.get(i), i);
        }
        
        for (int[] edge : edges) {
            int from = edge[0];
            int to = edge[1];
            if (position.get(from) >= position.get(to)) {
                return false;
            }
        }
        return true;
    }
    
    // Actual topological sort implementation for testing
    private List<Integer> topologicalSort(int[][] edges, int numNodes) {
        Map<Integer, List<Integer>> graph = new HashMap<>();
        int[] inDegree = new int[numNodes];
        
        // Build graph and calculate in-degrees
        for (int[] edge : edges) {
            int from = edge[0];
            int to = edge[1];
            graph.computeIfAbsent(from, k -> new ArrayList<>()).add(to);
            inDegree[to]++;
        }
        
        // Initialize queue with nodes of in-degree 0
        Queue<Integer> queue = new LinkedList<>();
        for (int i = 0; i < numNodes; i++) {
            if (inDegree[i] == 0) {
                queue.offer(i);
            }
        }
        
        List<Integer> result = new ArrayList<>();
        
        // Process nodes in topological order
        while (!queue.isEmpty()) {
            int node = queue.poll();
            result.add(node);
            
            // Update in-degrees of neighbors
            if (graph.containsKey(node)) {
                for (int neighbor : graph.get(node)) {
                    inDegree[neighbor]--;
                    if (inDegree[neighbor] == 0) {
                        queue.offer(neighbor);
                    }
                }
            }
        }
        
        // Check for cycles
        return result.size() == numNodes ? result : new ArrayList<>();
    }
}

Test Case Analysis

Basic Valid DAG: Tests fundamental functionality with a simple directed acyclic graph where nodes can be ordered topologically.

Single Node: Validates edge case handling for minimal inputs with no dependencies.

Cycle Detection: Ensures proper cycle detection and return of empty result when a cycle exists in the graph.

Multiple Valid Orderings: Confirms that multiple valid topological orderings are handled correctly and accepted as valid.

Empty Graph: Tests handling of graphs with no edges but multiple nodes, ensuring all nodes can be ordered.

The test cases cover the essential scenarios that a robust topological sort implementation must handle, including proper validation of results and edge case handling. Each test verifies that the algorithm produces correct output according to topological ordering constraints while properly detecting and handling cycles.

11. Final Summary

The core idea is to perform a topological sort using BFS with in-degree tracking to determine a valid ordering of tasks or items with dependencies, returning an empty array if a cycle exists. This solution works by leveraging the fundamental property that nodes with zero in-degree can only be processed when all their prerequisites are satisfied, making it both correct and optimal for dependency resolution problems. The approach is efficient with O(V + E) time and space complexity, where V represents the number of items and E represents the dependencies between them.

Was this article helpful?

Topics in this article

More from DSA Patterns

  • DSA Problems

    Max Area of Island Java Solution

    Complete Java implementation for finding maximum area of island in 2D grid using DFS and BFS approaches with time and space complexity analysis.

  • DSA Problems

    Number of Islands Problem Solution

    Complete Java implementation for counting connected land cells in a 2D grid using DFS and BFS approaches.

  • DSA Patterns

    Cycle Detection in Directed Graphs Using DFS

    Complete Java solution for detecting cycles in directed graphs with time complexity analysis and implementation trade-offs.

  • DSA Problems

    Subtree of Another Tree Java Solution

    Complete Java implementation for checking if one binary tree is a subtree of another, including multiple approaches and time complexity analysis.

Stay in the Loop

Get curated DSA patterns, coding problems, frontend and backend LLD/HLD, and AI/ML System Design insights delivered straight to your inbox. No spam, just focused interview and engineering content.