Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Topological Sort with DFS Post-Order Traversal

Welcome to our next lesson on graph algorithms. Last time, we took a deep dive into Kahn's algorithm, an intuitive, BFS-based approach to topological sorting that mirrors how you might schedule tasks by repeatedly picking those with no outstanding prerequisites.

Today, we'll explore the second major strategy for this problem: a method built around Depth-First Search (DFS). This approach leverages the natural structure of recursion to determine the ordering. You'll learn how the post-order traversal property of DFS is the key to this algorithm and how to implement it, including the critical logic for detecting cycles. Understanding both the BFS and DFS patterns for topological sort will give you a versatile toolkit for dependency-related problems in coding interviews.

By the end of this lesson, you will be able to implement a complete topological sort using a DFS-based, post-order traversal approach.

The Core Idea: Finishing Order is Key

In our last lesson, we built the topological sort by finding nodes with an in-degree of 0 and adding them to our list. The DFS approach works from the opposite end. It explores as far down a dependency chain as possible before adding anything to the list.

Think of a recursive DFS traversal on a graph. A function call for a node u will only finish and return after all of its descendants (the nodes it has edges to) have been fully explored. This moment—right before the function for u returns—is called the post-order position.

This property is exactly what we need for topological sort. A task can only be considered "complete" after all of its prerequisites are complete. In DFS terms, we only "finish" with a node u after we have finished with all the nodes v that u points to.

Therefore, the algorithm is:

  1. Perform a DFS traversal on the graph.
  2. When a node is "finished" (i.e., all its neighbors have been visited, and the recursion is about to backtrack from it), add it to a list. This list represents the post-order traversal.
  3. The final topological sort is simply the reverse of this post-order list.

A node that is finished last has no other nodes depending on it (in its DFS subtree), making it a candidate for the end of a topological sort. Conversely, a node that depends on many others will only be finished after all its dependencies are finished, placing it earlier in the post-order list and thus later in the final reversed (topological) order.

The following article from Labuladong's notes provides an excellent intuition for this, using a binary tree as a simple analogy before applying it to a general graph.

Cycle Detection and Topological Sort Algorithm

This section explains the core intuition behind the DFS-based approach.

Read the section explaining the logic. Focus on the connection between a node's dependencies and the post-order traversal property, where a node is processed only after its children (or dependencies) are fully traversed.

Visualizing the DFS Approach

Let's make this more concrete. The key steps are traversing deep, and then adding nodes to a list as we backtrack.

The image below shows a simple graph. A DFS traversal might start at node 0, go to 1, then 4. Node 4 has a neighbor 7, so we go there. Node 7 has no outgoing edges, so its DFS call finishes. We add 7 to our post-order list. We backtrack to 4, which has no more neighbors to visit. We add 4 to the list. This continues until the entire graph is traversed.

An example of DFS-based topological sort. A DFS traversal is performed, and nodes are recorded in a post-order list as their recursive calls finish. The final topological ordering is the reverse of this post-order list.

For an animated walkthrough, the following video by WilliamFiset is very helpful. It shows the call stack growing and shrinking, and how nodes are added to the ordering on the "recursive callback."

Topological Sort Algorithm | Graph Theory

This video, "Topological Sort Algorithm," demonstrates the DFS approach.

First, watch the brief introduction to the algorithm from here. Then, follow the animated example from this section. Observe how the algorithm picks a starting node, explores deeply, and adds nodes to the topological ordering as it backtracks up the call stack.

Implementation: DFS with Cycle Detection

A critical part of any topological sort algorithm is handling cycles. A topological sort is only defined for a Directed Acyclic Graph (DAG). If a cycle exists, it's impossible to create a valid ordering.

In a DFS-based approach, we can detect a cycle by keeping track of the nodes currently in our recursion stack. We use a three-state system for each node:

  1. Unvisited (White): The node has not been explored yet.
  2. Visiting (Gray): The node is currently in the recursion stack. We have started a DFS from this node, but we have not finished exploring all its descendants.
  3. Visited (Black): The DFS call for this node and all its descendants has completed.

A cycle is detected if, during our traversal from a node u, we encounter a neighbor v that is already in the "Visiting" (Gray) state. This means we have found a path back to a node that is one of its own ancestors in the current DFS path—a back edge, which defines a cycle.

This image compares Kahn's algorithm with the DFS-based approach. The right side illustrates the three-state system (unexplored, visiting, visited) used in DFS for cycle detection.

Now, let's put this all together into a concrete implementation plan, using the classic LeetCode problem "Course Schedule II" as our guide. Your task is to return a valid course order, or an empty list if a cycle exists.

The following video from NeetCode provides a fantastic, practical walkthrough of implementing this in Python, which is directly applicable to interview settings.

Course Schedule II - Topological Sort - Leetcode 210

This video, "Course Schedule II - Topological Sort," details a complete DFS solution for this classic problem.

Watch the initial problem breakdown and the high-level DFS strategy from this segment. Notice how he traces the DFS path and adds courses to the output as he backtracks. Pay close attention to the explanation of cycle detection from here, where he introduces the idea of tracking the current path. Follow the code setup from this part, where he creates the adjacency list and the visit and cycle sets to manage the three node states. The core logic is in the dfs function walkthrough from here. This is the most important part. Understand how the base cases (cycle found, already visited) work and where the node is added to the output (post-order). Finally, see how the main loop ties it all together by calling DFS for every course from the end.

The Algorithm in Pseudocode

Based on the concepts we've discussed, here is a structured summary of the algorithm:

  1. Initialization:

    • Build an adjacency list for the graph.
    • Initialize an empty list for the topological order (result).
    • Initialize two sets: visited (for black nodes) and recursion_stack (for gray nodes).
  2. Main Loop:

    • Iterate through each node i from 0 to N-1.
    • If i is not in visited, call dfs(i).
    • If dfs(i) returns False (indicating a cycle), stop and return an empty list.
  3. DFS Function (dfs(node)):

    • Base Case 1 (Cycle): If node is in recursion_stack, a cycle is detected. Return False.
    • Base Case 2 (Already Processed): If node is in visited, it has already been processed as part of another DFS path. Return True.
    • Mark as Visiting: Add node to recursion_stack.
    • Recursive Step: For each neighbor of node:
      • If dfs(neighbor) returns False, propagate the failure by returning False immediately.
    • Mark as Visited (Post-order):
      • Remove node from recursion_stack.
      • Add node to visited.
      • Append node to the result list.
    • Return True.
  4. Final Step:

    • If the main loop completes without detecting a cycle, the result list contains the post-order traversal. Return result reversed.

This implementation is robust, handling disconnected components and correctly identifying cycles. For a clean, text-based implementation, you can refer back to the Labuladong article.

Cycle Detection and Topological Sort Algorithm

This article provides a full implementation based on the Course Schedule II problem.

Review the solution code. This code integrates the cycle detection (hasCycle, onPath) and post-order recording (postorder.add(s)) into a single traverse function. Notice the final step is to reverse the postorder list.

Complexity Analysis

Just like Kahn's algorithm, the DFS-based approach is very efficient.

  • Time Complexity:

    • Each vertex v is visited exactly once. The dfs(v) function is called once for each vertex.
    • When inside dfs(v), we iterate through all of its outgoing edges. Over the entire algorithm, every edge e is traversed exactly once.
    • Therefore, the total time is proportional to the number of vertices and edges.
  • Space Complexity:

    • to store the adjacency list.
    • for the visited and recursion_stack sets.
    • for the recursion stack in the worst case (for a path graph).

Conclusion

In this lesson, we've dissected the DFS-based approach to topological sorting. By leveraging the post-order property of a depth-first search, we can construct a valid task ordering in a way that is both elegant and efficient. You are now equipped with two distinct, powerful patterns for solving problems involving dependencies.

Key Takeaways:

  • Core Logic: The reverse of a DFS post-order traversal on a DAG yields a valid topological sort.
  • Post-order Intuition: A node is added to the post-order list only after all its dependencies have been fully explored and added.
  • Cycle Detection: A three-state system (unvisited, visiting, visited) is used. A cycle is found if we encounter a "visiting" node.
  • Implementation: The algorithm is naturally recursive. A main loop ensures all graph components are visited, and a helper DFS function handles traversal, cycle detection, and building the post-order list.
  • Comparison: While Kahn's algorithm is iterative (BFS), the DFS approach is recursive. Both have the same time complexity and are valid choices for interview problems.

In our next lesson, we will move beyond just implementing these algorithms. We'll focus on applying them to solve a variety of interview-style problems involving task scheduling, dependency resolution, and finding the shortest/longest path in DAGs.

Can't find a good explanation? Sign up and we'll make it for you

Sign up