6 min read

Understanding and Applying Topological Sort

Topological sorting is a linear ordering of nodes in a directed acyclic graph (DAG) such that for every directed edge from node A to node B, node A appears before node B in the ordering. This means you can only move to a node after all its prerequisites have been fulfilled. Think of it like a recipe where you can't add the sugar until you've mixed the flour and eggs.

Identifying Topological Sort Problems

Several problem types hint at the need for topological sorting. Look for these common indicators:

  • Dependencies: The problem describes relationships where one task or element depends on the completion of others. Examples include course prerequisites, build processes (like compiling code), or task scheduling.
  • Directed Acyclic Graph (DAG): The problem's inherent structure can be represented as a DAG. If you can visually represent the relationships as a graph with unidirectional arrows and no cycles (loops), topological sort is likely applicable.
  • Ordering/Sequencing: The problem requires ordering elements or tasks in a specific sequence that respects the dependencies.

How to Check for Topological Sort

When encountering a problem, ask yourself: Can I represent this problem as a directed acyclic graph where edges represent dependencies? If the answer is yes, topological sort is a strong candidate.


flowchart LR
    A["Course A (in-degree: 0)"] --> C["Course C (in-degree: 2)"]
    B["Course B (in-degree: 0)"] --> C
    C --> D["Course D (in-degree: 1)"]
    C --> E["Course E (in-degree: 1)"]
    D --> F["Course F (in-degree: 2)"]
    E --> F

flowchart TD
    Step1["1. Calculate in-degree for all nodes"] --> Step2["2. Enqueue all nodes with in-degree == 0"]
    Step2 --> WhileQueue{"Queue is not empty ?"}
    WhileQueue -- Yes --> Dequeue["3. Poll node U and add to Sorted Order"]
    Dequeue --> Decrement["4. For each neighbor V of U:
inDegree[V]--"]
    Decrement --> CheckZero{"inDegree[V] == 0 ?"}
    CheckZero -- Yes --> EnqueueV["Enqueue neighbor V"]
    CheckZero -- No --> WhileQueue
    EnqueueV --> WhileQueue
    WhileQueue -- No --> Done["Topological Order Complete
(If count < V, cycle detected!)"]

Algorithms and Code Examples

The most common algorithms for topological sorting are Kahn's algorithm and Depth-First Search (DFS) based approaches.

Kahn's Algorithm (Java):

import java.util.*;

public class TopologicalSort {

    public static List<Integer> kahnSort(Map<Integer, List<Integer>> graph) {
        Map<Integer, Integer> inDegree = new HashMap<>();
        for (int node : graph.keySet()) {
            inDegree.put(node, 0);
        }
        for (int node : graph.keySet()) {
            for (int neighbor : graph.get(node)) {
                inDegree.put(neighbor, inDegree.get(neighbor) + 1);
            }
        }

        Queue<Integer> queue = new LinkedList<>();
        for (int node : inDegree.keySet()) {
            if (inDegree.get(node) == 0) {
                queue.offer(node);
            }
        }

        List<Integer> sorted = new ArrayList<>();
        while (!queue.isEmpty()) {
            int node = queue.poll();
            sorted.add(node);
            for (int neighbor : graph.getOrDefault(node, new ArrayList<>())) {
                inDegree.put(neighbor, inDegree.get(neighbor) - 1);
                if (inDegree.get(neighbor) == 0) {
                    queue.offer(neighbor);
                }
            }
        }

        return sorted.size() == graph.size() ? sorted : new ArrayList<>(); //Check for cycles
    }

    public static void main(String[] args) {
        //Example graph representation (adjacency list)
        Map<Integer, List<Integer>> graph = new HashMap<>();
        graph.put(5, Arrays.asList(2, 0));
        graph.put(4, Arrays.asList(0, 1));
        graph.put(2, Arrays.asList(3));
        graph.put(3, Arrays.asList(1));
        graph.put(0, new ArrayList<>());
        graph.put(1, new ArrayList<>());

        System.out.println(kahnSort(graph)); //Example Output: [5, 4, 2, 3, 0, 1] (order may vary)

    }
}

Python (DFS):

def topological_sort_dfs(graph):
    visited = set()
    stack = []

    def dfs(node):
        visited.add(node)
        for neighbor in graph.get(node, []):
            if neighbor not in visited:
                dfs(neighbor)
        stack.append(node)

    for node in graph:
        if node not in visited:
            dfs(node)
    return stack[::-1] #Reverse the stack for correct order

# Example graph (adjacency list)
graph = {
    5: [2, 0],
    4: [0, 1],
    2: [3],
    3: [1],
    0: [],
    1: []
}
print(topological_sort_dfs(graph)) #Example Output: [5, 4, 2, 3, 0, 1] (order may vary)

JavaScript (Kahn's Algorithm):

function topologicalSort(graph) {
  const inDegree = {};
  for (const node in graph) {
    inDegree[node] = 0;
  }
  for (const node in graph) {
    for (const neighbor of graph[node]) {
      inDegree[neighbor]++;
    }
  }

  const queue = [];
  for (const node in inDegree) {
    if (inDegree[node] === 0) {
      queue.push(node);
    }
  }

  const sorted = [];
  while (queue.length > 0) {
    const node = queue.shift();
    sorted.push(node);
    for (const neighbor of graph[node] || []) {
      inDegree[neighbor]--;
      if (inDegree[neighbor] === 0) {
        queue.push(neighbor);
      }
    }
  }

  return sorted.length === Object.keys(graph).length ? sorted : []; //Check for cycles
}


// Example graph (adjacency list)
const graph = {
  '5': ['2', '0'],
  '4': ['0', '1'],
  '2': ['3'],
  '3': ['1'],
  '0': [],
  '1': []
};

console.log(topologicalSort(graph)); //Example Output: ['5', '4', '2', '3', '0', '1'] (order may vary)

These examples show how to implement topological sorting using different programming languages. Remember to handle cases with cycles appropriately (the examples include cycle detection).

  • Khan Academy - Graph Algorithms


  • Mastering Algorithms: A Deep Dive into Topological Sorting

    Topological sorting is a crucial algorithm for directed acyclic graphs (DAGs). It provides a linear ordering of nodes such that for every directed edge from node A to node B, node A appears before node B in the ordering. This is fundamental in various applications, from dependency resolution in software projects to scheduling tasks with prerequisites.

    Understanding the Pattern

    The core idea behind topological sorting is to find nodes with no incoming edges (in-degree 0). These nodes can be processed first, as they don't depend on any other nodes. After processing a node, we remove its outgoing edges, potentially revealing new nodes with in-degree 0. We repeat this process until all nodes are processed. If a cycle exists (a node depends on itself directly or indirectly), topological sorting is impossible.

    Step-by-Step Approach

    1. Calculate In-degrees: For each node, count the number of incoming edges.
    2. Find Starting Nodes: Identify nodes with an in-degree of 0. These form our starting points.
    3. Process Nodes: Iteratively:
      • Choose a node with in-degree 0.
      • Add it to the sorted list.
      • Remove its outgoing edges (decrement the in-degree of its neighbors).
    4. Cycle Detection: If, at any point, you cannot find a node with in-degree 0 and there are still unprocessed nodes, a cycle exists, and topological sorting is not possible.

    Example: Course Scheduling

    Imagine scheduling courses where some courses have prerequisites. We can represent this as a DAG:

          Course A
           |
           v
         Course B
         /   \
        /     \
    Course C   Course D
           \   /
            \ /
          Course E
    

    Here, Course B requires Course A, Course E requires both Course C and Course D, and so on.

    Java Implementation:

    import java.util.*;
    
    public class TopologicalSort {
        public static List<Integer> topologicalSort(Map<Integer, List<Integer>> graph) {
            Map<Integer, Integer> inDegree = new HashMap<>();
            for (int node : graph.keySet()) {
                inDegree.put(node, 0);
            }
            for (int node : graph.keySet()) {
                for (int neighbor : graph.get(node)) {
                    inDegree.put(neighbor, inDegree.get(neighbor) + 1);
                }
            }
    
            Queue<Integer> queue = new LinkedList<>();
            for (int node : inDegree.keySet()) {
                if (inDegree.get(node) == 0) {
                    queue.offer(node);
                }
            }
    
            List<Integer> sortedList = new ArrayList<>();
            while (!queue.isEmpty()) {
                int node = queue.poll();
                sortedList.add(node);
                for (int neighbor : graph.getOrDefault(node, new ArrayList<>())) {
                    inDegree.put(neighbor, inDegree.get(neighbor) - 1);
                    if (inDegree.get(neighbor) == 0) {
                        queue.offer(neighbor);
                    }
                }
            }
    
            if (sortedList.size() != graph.size()) {
                return new ArrayList<>(); // Cycle detected
            }
            return sortedList;
        }
    
        public static void main(String[] args) {
            Map<Integer, List<Integer>> graph = new HashMap<>();
            graph.put(1, Arrays.asList(2));
            graph.put(2, Arrays.asList(3, 4));
            graph.put(3, Arrays.asList(5));
            graph.put(4, Arrays.asList(5));
    
            List<Integer> sorted = topologicalSort(graph);
            System.out.println(sorted); // Possible output: [1, 2, 3, 4, 5] (order may vary)
    
        }
    }
    

    Python Implementation:

    from collections import defaultdict
    
    def topological_sort(graph):
        in_degree = defaultdict(int)
        for node in graph:
            for neighbor in graph[node]:
                in_degree[neighbor] += 1
    
        queue = [node for node in graph if in_degree[node] == 0]
        sorted_list = []
    
        while queue:
            node = queue.pop(0)
            sorted_list.append(node)
            for neighbor in graph.get(node, []):
                in_degree[neighbor] -= 1
                if in_degree[neighbor] == 0:
                    queue.append(neighbor)
    
        if len(sorted_list) != len(graph):
            return []  # Cycle detected
    
        return sorted_list
    
    graph = {
        1: [2],
        2: [3, 4],
        3: [5],
        4: [5]
    }
    
    print(topological_sort(graph)) # Possible output: [1, 2, 3, 4, 5] (order may vary)
    

    JavaScript Implementation:

    function topologicalSort(graph) {
      const inDegree = {};
      for (const node in graph) {
        inDegree[node] = 0;
      }
      for (const node in graph) {
        for (const neighbor of graph[node]) {
          inDegree[neighbor]++;
        }
      }
    
      const queue = [];
      for (const node in inDegree) {
        if (inDegree[node] === 0) {
          queue.push(node);
        }
      }
    
      const sortedList = [];
      while (queue.length > 0) {
        const node = queue.shift();
        sortedList.push(node);
        for (const neighbor of graph[node] || []) {
          inDegree[neighbor]--;
          if (inDegree[neighbor] === 0) {
            queue.push(neighbor);
          }
        }
      }
    
      if (sortedList.length !== Object.keys(graph).length) {
        return []; // Cycle detected
      }
      return sortedList;
    }
    
    const graph = {
      '1': ['2'],
      '2': ['3', '4'],
      '3': ['5'],
      '4': ['5']
    };
    
    console.log(topologicalSort(graph)); //Possible output: [ '1', '2', '3', '4', '5' ] (order may vary)
    

    Example Problems

    1. Course Scheduling: As shown above.
    2. Dependency Resolution in Software: Ordering the compilation of source files based on their dependencies.
    3. Build Systems (Makefiles): Determining the order to build targets.
    4. Data Serialization: Ordering data elements for efficient storage or transmission.
    5. Instruction Scheduling in Compilers: Optimizing instruction execution order.
    6. Network Packet Ordering: Reconstructing the original order of packets received out of sequence.
    7. Alien Dictionary: Given a set of words, determine if they can be ordered lexicographically such that all pairs of words satisfy a given precedence relation.
    8. Preprocessing steps in a pipeline: Order tasks in a data pipeline based on input-output dependencies.
    9. Task scheduling on a single processor: Determine the optimal schedule for tasks with dependencies and durations.
    10. Algorithm design for finding strongly connected components: Topological sorting can be used as a subroutine in algorithms to find strongly connected components in a directed graph.

    Mastering Topological Sort: A Programmer's Guide

    Topological sorting is a crucial algorithm in graph theory, useful for ordering elements in a way that respects dependencies. It's applicable to a wide range of problems, from scheduling tasks to resolving dependencies in software build systems. This post explores the core concepts, provides example problems, and offers solutions in Java, Python, and JavaScript.

    Understanding Topological Sort

    A topological sort of a directed acyclic graph (DAG) is a linear ordering of its vertices such that for every directed edge from vertex u to vertex v, vertex u comes before vertex v in the ordering. Crucially, a topological sort is only possible if the graph is acyclic (contains no cycles). The presence of a cycle indicates a circular dependency that cannot be resolved linearly.

    Identifying the Pattern: The key pattern is identifying dependencies. If task A depends on task B, then B must come before A in the sorted order. This forms the basis for algorithms that detect cycles and generate the sorted order.

    Simple Approach: Kahn's Algorithm

    Kahn's algorithm is a widely used approach for topological sorting. It's based on repeatedly removing nodes with no incoming edges.

    Steps:

    1. Find nodes with in-degree 0: In-degree represents the number of incoming edges to a node.
    2. Add these nodes to the result: These can be processed first since they have no dependencies.
    3. Remove these nodes and update in-degree: Reduce the in-degree of their neighbors.
    4. Repeat steps 1-3: Continue until all nodes are processed or a cycle is detected (no nodes with in-degree 0 remain).

    Code Examples

    Java:

    import java.util.*;
    
    public class TopologicalSort {
        public static List<Integer> topologicalSort(Map<Integer, List<Integer>> graph) {
            Map<Integer, Integer> inDegree = new HashMap<>();
            for (int node : graph.keySet()) {
                inDegree.put(node, 0);
            }
            for (int node : graph.keySet()) {
                for (int neighbor : graph.get(node)) {
                    inDegree.put(neighbor, inDegree.get(neighbor) + 1);
                }
            }
    
            Queue<Integer> queue = new LinkedList<>();
            for (int node : inDegree.keySet()) {
                if (inDegree.get(node) == 0) {
                    queue.offer(node);
                }
            }
    
            List<Integer> result = new ArrayList<>();
            while (!queue.isEmpty()) {
                int node = queue.poll();
                result.add(node);
                for (int neighbor : graph.getOrDefault(node, new ArrayList<>())) {
                    inDegree.put(neighbor, inDegree.get(neighbor) - 1);
                    if (inDegree.get(neighbor) == 0) {
                        queue.offer(neighbor);
                    }
                }
            }
    
            return result.size() == graph.size() ? result : new ArrayList<>(); //Check for cycles
        }
    
        public static void main(String[] args) {
            //Example graph represented as an adjacency list
            Map<Integer, List<Integer>> graph = new HashMap<>();
            graph.put(5, Arrays.asList(2, 0));
            graph.put(4, Arrays.asList(0, 1));
            graph.put(2, Arrays.asList(3));
            graph.put(3, Arrays.asList(1));
            graph.put(0, new ArrayList<>());
            graph.put(1, new ArrayList<>());
    
            System.out.println(topologicalSort(graph));
        }
    }
    

    Python:

    from collections import defaultdict
    
    def topological_sort(graph):
        in_degree = defaultdict(int)
        for node in graph:
            for neighbor in graph[node]:
                in_degree[neighbor] += 1
    
        queue = [node for node in graph if in_degree[node] == 0]
        result = []
        while queue:
            node = queue.pop(0)
            result.append(node)
            for neighbor in graph[node]:
                in_degree[neighbor] -= 1
                if in_degree[neighbor] == 0:
                    queue.append(neighbor)
        return result if len(result) == len(graph) else [] #Check for cycles
    
    # Example graph represented as an adjacency list
    graph = {
        5: [2, 0],
        4: [0, 1],
        2: [3],
        3: [1],
        0: [],
        1: []
    }
    
    print(topological_sort(graph))
    

    JavaScript:

    function topologicalSort(graph) {
        const inDegree = {};
        for (const node in graph) {
            inDegree[node] = 0;
        }
        for (const node in graph) {
            for (const neighbor of graph[node]) {
                inDegree[neighbor]++;
            }
        }
    
        const queue = [];
        for (const node in inDegree) {
            if (inDegree[node] === 0) {
                queue.push(node);
            }
        }
    
        const result = [];
        while (queue.length > 0) {
            const node = queue.shift();
            result.push(node);
            for (const neighbor of graph[node] || []) {
                inDegree[neighbor]--;
                if (inDegree[neighbor] === 0) {
                    queue.push(neighbor);
                }
            }
        }
        return result.length === Object.keys(graph).length ? result : []; //Check for cycles
    }
    
    
    // Example graph represented as an adjacency list
    const graph = {
        '5': ['2', '0'],
        '4': ['0', '1'],
        '2': ['3'],
        '3': ['1'],
        '0': [],
        '1': []
    };
    
    console.log(topologicalSort(graph));
    

    Example Problems (Difficulty Levels)

    Easy:

    1. Course Schedule: Given a list of courses and their prerequisites, determine if it's possible to finish all courses.

    Medium:

    1. Alien Dictionary: Given a list of words from an alien language, reconstruct the order of the alphabet.
    2. Task Scheduling Order: Determine a valid order to execute a set of tasks with dependencies.

    Hard:

    1. Minimum Height Trees: Find the root nodes of minimum height trees in a forest.
    2. Course Schedule II: Given a list of courses and their prerequisites, return any valid ordering of courses that allows all courses to be taken.
    3. Reconstructing Itinerary: Given a list of tickets, reconstruct the itinerary in lexicographical order.
    4. Network Delay Time: Find the minimum time to send a signal through a network. (This problem can be solved using topological sort and Dijkstra's algorithm).
    5. Longest Path in a DAG: Find the length of the longest path in a directed acyclic graph.


    Related Guides:
    ← PreviousTop 'K' Elements TechniqueNext →Tree Breadth-First Search (BFS)