Skip to content

Topological Sort Guide

Introduction

This guide covers how to use the topologicalSort function to order vertices in a Directed Acyclic Graph (DAG) such that for every directed edge from vertex A to vertex B, A comes before B in the ordering. This is essential for resolving dependencies and determining execution order.

Basic Usage

The topologicalSort function provides a simple way to determine the correct order of operations in dependency graphs.

Simple Example

typescript
import { createGraph, topologicalSort } from "@apexds/core";
import { EnumGraphType } from "@apexds/core";

// Create a simple dependency graph
const graph = createGraph<string>(EnumGraphType.DIRECTED);
graph.addVertex("Task A");
graph.addVertex("Task B");
graph.addVertex("Task C");

// Define dependencies
graph.addEdge("Task A", "Task B"); // B depends on A
graph.addEdge("Task B", "Task C"); // C depends on B

// Get execution order
const order = topologicalSort(graph);
console.log(order); // ["Task A", "Task B", "Task C"]

Algorithm Overview

Topological sorting arranges vertices in a linear order that respects the graph's dependencies. The algorithm uses depth-first search (DFS) to traverse the graph and build the ordering.

How It Works

  1. Start with any vertex that has no incoming edges
  2. Add it to the result and remove it from the graph
  3. Repeat with remaining vertices
  4. Continue until all vertices are processed

The implementation uses a DFS-based approach with a recursion stack to detect cycles.

Practical Examples

Build System Pipeline

Model a software build process with dependencies:

typescript
// Create a build pipeline graph
const buildGraph = createGraph<string>(EnumGraphType.DIRECTED);

// Define build stages
const stages = ["Initialize", "Lint", "Compile", "Test", "Package", "Deploy"];
stages.forEach(stage => buildGraph.addVertex(stage));

// Define dependencies
buildGraph.addEdge("Initialize", "Lint");
buildGraph.addEdge("Initialize", "Compile");
buildGraph.addEdge("Lint", "Compile");
buildGraph.addEdge("Compile", "Test");
buildGraph.addEdge("Test", "Package");
buildGraph.addEdge("Package", "Deploy");

// Get build order
const buildOrder = topologicalSort(buildGraph);
console.log("Build execution order:", buildOrder);

// Execute the build pipeline
async function runBuild() {
  for (const stage of buildOrder) {
    console.log("Running:", stage);
    await executeStage(stage);
  }
}

Course Prerequisite Planning

Help students plan their course schedule based on prerequisites:

typescript
// Create a course dependency graph
const curriculum = createGraph<string>(EnumGraphType.DIRECTED);

// Add computer science courses
const courses = [
  "CS101: Introduction to Programming",
  "CS201: Data Structures",
  "CS301: Algorithms",
  "CS302: Database Systems",
  "CS401: Web Development",
  "CS402: Machine Learning"
];

courses.forEach(course => curriculum.addVertex(course));

// Define prerequisites
curriculum.addEdge("CS101: Introduction to Programming", "CS201: Data Structures");
curriculum.addEdge("CS201: Data Structures", "CS301: Algorithms");
curriculum.addEdge("CS201: Data Structures", "CS302: Database Systems");
curriculum.addEdge("CS201: Data Structures", "CS401: Web Development");
curriculum.addEdge("CS301: Algorithms", "CS402: Machine Learning");
curriculum.addEdge("CS302: Database Systems", "CS402: Machine Learning");
curriculum.addEdge("CS401: Web Development", "CS402: Machine Learning");

// Get recommended study order
const studyPlan = topologicalSort(curriculum);
console.log("Recommended semester-by-semester plan:");

// Group by semester (simplified)
const semesters: string[][] = [[], [], [], [], [], []];
studyPlan.forEach(course => {
  // In a real implementation, you would use more sophisticated grouping
  const semesterIndex = Math.min(Math.floor(studyPlan.indexOf(course) / 2), 5);
  semesters[semesterIndex].push(course);
});

semesters.forEach((semester, index) => {
  if (semester.length > 0) {
    console.log(`Semester ${index + 1}: ${semester.join(', ')}`);
  }
});

Package Dependency Resolution

Model package manager dependency resolution:

typescript
// Create a package dependency graph
const packageManager = createGraph<string>(EnumGraphType.DIRECTED);

// Add packages
const packages = [
  "lodash", "axios", "react", "react-dom", "redux", "react-redux", "typescript"
];

packages.forEach(pkg => packageManager.addVertex(pkg));

// Define dependencies
packageManager.addEdge("react", "react-dom");
packageManager.addEdge("react-redux", "react");
packageManager.addEdge("react-redux", "redux");
packageManager.addEdge("redux", "lodash");

// Get installation order
const installOrder = topologicalSort(packageManager);
console.log("Package installation order:", installOrder);

// Generate installation commands
installOrder.forEach(pkg => {
  console.log(`npm install ${pkg}`);
});

Task Scheduler

Create a task scheduler for a project management system:

typescript
// Create a project task graph
const project = createGraph<string>(EnumGraphType.DIRECTED);

// Define project tasks
const tasks = [
  "Project Kickoff",
  "Requirements Gathering",
  "System Design",
  "Database Design",
  "Frontend Development",
  "Backend Development",
  "Integration",
  "Testing",
  "Deployment",
  "Documentation"
];

tasks.forEach(task => project.addVertex(task));

// Define task dependencies
project.addEdge("Project Kickoff", "Requirements Gathering");
project.addEdge("Requirements Gathering", "System Design");
project.addEdge("System Design", "Database Design");
project.addEdge("System Design", "Frontend Development");
project.addEdge("System Design", "Backend Development");
project.addEdge("Database Design", "Backend Development");
project.addEdge("Frontend Development", "Integration");
project.addEdge("Backend Development", "Integration");
project.addEdge("Integration", "Testing");
project.addEdge("Testing", "Deployment");
project.addEdge("Testing", "Documentation");
project.addEdge("Deployment", "Documentation");

// Get project timeline
const timeline = topologicalSort(project);
console.log("Project execution timeline:", timeline);

// Calculate critical path (simplified)
const criticalPath: string[] = [];
timeline.forEach(task => {
  // In a real implementation, you would consider task durations
  // and calculate the longest path through the project
  criticalPath.push(task);
});

Error Handling

The topologicalSort function includes robust error handling for common graph issues.

Cycle Detection

typescript
try {
  // Create a graph with a cycle
  const cyclicGraph = createGraph<string>(EnumGraphType.DIRECTED);
  cyclicGraph.addVertex("A");
  cyclicGraph.addVertex("B");
  cyclicGraph.addEdge("A", "B");
  cyclicGraph.addEdge("B", "A"); // This creates a cycle
  
  // This will throw an error
  const result = topologicalSort(cyclicGraph);
} catch (error) {
  if (error instanceof IllegalStateException) {
    console.error("Cycle detected in graph:", error.message);
    // Handle the error - perhaps by removing edges or notifying the user
  }
}

Undirected Graphs

typescript
try {
  // Topological sort is not defined for undirected graphs
  const undirected = createGraph<string>(EnumGraphType.UNDIRECTED);
  undirected.addVertex("A");
  undirected.addVertex("B");
  undirected.addEdge("A", "B");
  
  const result = topologicalSort(undirected);
} catch (error) {
  if (error instanceof IllegalStateException) {
    console.error("Topological sort requires a directed graph:", error.message);
  }
}

Advanced Usage

Partial Ordering

Sometimes you may want to sort only a subset of vertices:

typescript
function topologicalSortSubset<T>(
  graph: IGraph<T>, 
  subset: T[]
): T[] {
  // Create a subgraph with only the subset vertices and their connections
  const subgraph = createGraph<T>(EnumGraphType.DIRECTED);
  
  // Add vertices from subset
  subset.forEach(vertex => subgraph.addVertex(vertex));
  
  // Add edges between subset vertices
  subset.forEach(from => {
    subset.forEach(to => {
      if (from !== to && graph.hasEdge(from, to)) {
        subgraph.addEdge(from, to, graph.getEdgeWeight(from, to));
      }
    });
  });
  
  return topologicalSort(subgraph);
}

Multiple Valid Orderings

Graphs may have multiple valid topological orderings. You can implement strategies to choose among them:

typescript
// Strategy: Prioritize vertices with more dependencies (higher in-degree)
function topologicalSortWithPriority<T>(
  graph: IGraph<T>,
  priorityFn: (vertex: T) => number = () => 0
): T[] {
  const baseOrder = topologicalSort(graph);
  
  // Sort vertices within the topological constraints by priority
n  // (Implementation would need to respect dependency constraints)
}

Best Practices

Validate Input

Always ensure your graph is a DAG before calling topologicalSort:

typescript
function isAcyclic<T>(graph: IGraph<T>): boolean {
  try {
    topologicalSort(graph);
    return true;
  } catch (error) {
    return false;
  }
}

// Safe usage
if (isAcyclic(myGraph)) {
  const order = topologicalSort(myGraph);
  // Process the order
} else {
  console.log("Graph contains cycles - cannot perform topological sort");
}

Handle Errors Gracefully

typescript
function safeTopologicalSort<T>(
  graph: IGraph<T>
): T[] | null {
  try {
    return topologicalSort(graph);
  } catch (error) {
    console.error("Topological sort failed:", error);
    return null;
  }
}

Performance Considerations

  • Time complexity: O(V + E) where V is vertices and E is edges
  • Space complexity: O(V) for the recursion stack and visited maps
  • For very large graphs, consider iterative implementations to avoid stack overflow
  • The algorithm efficiently handles sparse and dense graphs
  • Edge weights are ignored in the sorting process

Next Steps

After mastering topological sort, explore:

MIT Licensed