From 2210a14597019e7c408f05008e66af5da9555aa5 Mon Sep 17 00:00:00 2001 From: codomposer Date: Mon, 10 Nov 2025 20:12:58 -0500 Subject: [PATCH 1/3] feat: add topological sort test alogrithmm and test script --- .../Graph/TopologicalSortTests.cs | 573 ++++++++++++++++++ Algorithms/Graph/TopologicalSort.cs | 290 +++++++++ 2 files changed, 863 insertions(+) create mode 100644 Algorithms.Tests/Graph/TopologicalSortTests.cs create mode 100644 Algorithms/Graph/TopologicalSort.cs diff --git a/Algorithms.Tests/Graph/TopologicalSortTests.cs b/Algorithms.Tests/Graph/TopologicalSortTests.cs new file mode 100644 index 00000000..944e3d7c --- /dev/null +++ b/Algorithms.Tests/Graph/TopologicalSortTests.cs @@ -0,0 +1,573 @@ +using Algorithms.Graph; +using DataStructures.Graph; + +namespace Algorithms.Tests.Graph; + +public class TopologicalSortTests +{ + /// + /// Test topological sort on a simple linear DAG: A → B → C. + /// Expected order: [A, B, C]. + /// + [Test] + public void Sort_SimpleLinearGraph_ReturnsCorrectOrder() + { + // Arrange + var graph = new DirectedWeightedGraph(3); + var vertexA = graph.AddVertex("A"); + var vertexB = graph.AddVertex("B"); + var vertexC = graph.AddVertex("C"); + + graph.AddEdge(vertexA, vertexB, 1); + graph.AddEdge(vertexB, vertexC, 1); + + var topologicalSort = new TopologicalSort(); + + // Act + var result = topologicalSort.Sort(graph); + + // Assert + Assert.That(result.Count, Is.EqualTo(3)); + Assert.That(result[0], Is.EqualTo(vertexA)); + Assert.That(result[1], Is.EqualTo(vertexB)); + Assert.That(result[2], Is.EqualTo(vertexC)); + } + + /// + /// Test Kahn's algorithm on a simple linear DAG: A → B → C. + /// Expected order: [A, B, C]. + /// + [Test] + public void SortKahn_SimpleLinearGraph_ReturnsCorrectOrder() + { + // Arrange + var graph = new DirectedWeightedGraph(3); + var vertexA = graph.AddVertex("A"); + var vertexB = graph.AddVertex("B"); + var vertexC = graph.AddVertex("C"); + + graph.AddEdge(vertexA, vertexB, 1); + graph.AddEdge(vertexB, vertexC, 1); + + var topologicalSort = new TopologicalSort(); + + // Act + var result = topologicalSort.SortKahn(graph); + + // Assert + Assert.That(result.Count, Is.EqualTo(3)); + Assert.That(result[0], Is.EqualTo(vertexA)); + Assert.That(result[1], Is.EqualTo(vertexB)); + Assert.That(result[2], Is.EqualTo(vertexC)); + } + + /// + /// Test topological sort on a DAG with multiple valid orderings. + /// Graph: A → C + /// B → C + /// Valid orderings: [A, B, C] or [B, A, C]. + /// We verify that C comes after both A and B. + /// + [Test] + public void Sort_GraphWithMultipleValidOrderings_ReturnsValidOrder() + { + // Arrange + var graph = new DirectedWeightedGraph(3); + var vertexA = graph.AddVertex("A"); + var vertexB = graph.AddVertex("B"); + var vertexC = graph.AddVertex("C"); + + graph.AddEdge(vertexA, vertexC, 1); + graph.AddEdge(vertexB, vertexC, 1); + + var topologicalSort = new TopologicalSort(); + + // Act + var result = topologicalSort.Sort(graph); + + // Assert + Assert.That(result.Count, Is.EqualTo(3)); + + // C should come after both A and B + var indexA = result.IndexOf(vertexA); + var indexB = result.IndexOf(vertexB); + var indexC = result.IndexOf(vertexC); + + Assert.That(indexC, Is.GreaterThan(indexA)); + Assert.That(indexC, Is.GreaterThan(indexB)); + } + + /// + /// Test Kahn's algorithm on a DAG with multiple valid orderings. + /// Graph: A → C + /// B → C + /// Valid orderings: [A, B, C] or [B, A, C]. + /// We verify that C comes after both A and B. + /// + [Test] + public void SortKahn_GraphWithMultipleValidOrderings_ReturnsValidOrder() + { + // Arrange + var graph = new DirectedWeightedGraph(3); + var vertexA = graph.AddVertex("A"); + var vertexB = graph.AddVertex("B"); + var vertexC = graph.AddVertex("C"); + + graph.AddEdge(vertexA, vertexC, 1); + graph.AddEdge(vertexB, vertexC, 1); + + var topologicalSort = new TopologicalSort(); + + // Act + var result = topologicalSort.SortKahn(graph); + + // Assert + Assert.That(result.Count, Is.EqualTo(3)); + + // C should come after both A and B + var indexA = result.IndexOf(vertexA); + var indexB = result.IndexOf(vertexB); + var indexC = result.IndexOf(vertexC); + + Assert.That(indexC, Is.GreaterThan(indexA)); + Assert.That(indexC, Is.GreaterThan(indexB)); + } + + /// + /// Test topological sort on a more complex DAG. + /// Graph: A → B → D + /// A → C → D + /// Valid orderings include: [A, B, C, D], [A, C, B, D]. + /// We verify that A comes first and D comes last. + /// + [Test] + public void Sort_ComplexDAG_ReturnsValidOrder() + { + // Arrange + var graph = new DirectedWeightedGraph(4); + var vertexA = graph.AddVertex("A"); + var vertexB = graph.AddVertex("B"); + var vertexC = graph.AddVertex("C"); + var vertexD = graph.AddVertex("D"); + + graph.AddEdge(vertexA, vertexB, 1); + graph.AddEdge(vertexA, vertexC, 1); + graph.AddEdge(vertexB, vertexD, 1); + graph.AddEdge(vertexC, vertexD, 1); + + var topologicalSort = new TopologicalSort(); + + // Act + var result = topologicalSort.Sort(graph); + + // Assert + Assert.That(result.Count, Is.EqualTo(4)); + Assert.That(result[0], Is.EqualTo(vertexA)); // A must be first + Assert.That(result[3], Is.EqualTo(vertexD)); // D must be last + + // B and C should come after A and before D + var indexB = result.IndexOf(vertexB); + var indexC = result.IndexOf(vertexC); + + Assert.That(indexB, Is.GreaterThan(0)); + Assert.That(indexB, Is.LessThan(3)); + Assert.That(indexC, Is.GreaterThan(0)); + Assert.That(indexC, Is.LessThan(3)); + } + + /// + /// Test Kahn's algorithm on a more complex DAG. + /// Graph: A → B → D + /// A → C → D + /// Valid orderings include: [A, B, C, D], [A, C, B, D]. + /// We verify that A comes first and D comes last. + /// + [Test] + public void SortKahn_ComplexDAG_ReturnsValidOrder() + { + // Arrange + var graph = new DirectedWeightedGraph(4); + var vertexA = graph.AddVertex("A"); + var vertexB = graph.AddVertex("B"); + var vertexC = graph.AddVertex("C"); + var vertexD = graph.AddVertex("D"); + + graph.AddEdge(vertexA, vertexB, 1); + graph.AddEdge(vertexA, vertexC, 1); + graph.AddEdge(vertexB, vertexD, 1); + graph.AddEdge(vertexC, vertexD, 1); + + var topologicalSort = new TopologicalSort(); + + // Act + var result = topologicalSort.SortKahn(graph); + + // Assert + Assert.That(result.Count, Is.EqualTo(4)); + Assert.That(result[0], Is.EqualTo(vertexA)); // A must be first + Assert.That(result[3], Is.EqualTo(vertexD)); // D must be last + + // B and C should come after A and before D + var indexB = result.IndexOf(vertexB); + var indexC = result.IndexOf(vertexC); + + Assert.That(indexB, Is.GreaterThan(0)); + Assert.That(indexB, Is.LessThan(3)); + Assert.That(indexC, Is.GreaterThan(0)); + Assert.That(indexC, Is.LessThan(3)); + } + + /// + /// Test topological sort on a graph with a cycle. + /// Graph: A → B → C → A (cycle). + /// Should throw InvalidOperationException. + /// + [Test] + public void Sort_GraphWithCycle_ThrowsInvalidOperationException() + { + // Arrange + var graph = new DirectedWeightedGraph(3); + var vertexA = graph.AddVertex("A"); + var vertexB = graph.AddVertex("B"); + var vertexC = graph.AddVertex("C"); + + graph.AddEdge(vertexA, vertexB, 1); + graph.AddEdge(vertexB, vertexC, 1); + graph.AddEdge(vertexC, vertexA, 1); // Creates a cycle + + var topologicalSort = new TopologicalSort(); + + // Act & Assert + Assert.Throws(() => topologicalSort.Sort(graph)); + } + + /// + /// Test Kahn's algorithm on a graph with a cycle. + /// Graph: A → B → C → A (cycle). + /// Should throw InvalidOperationException. + /// + [Test] + public void SortKahn_GraphWithCycle_ThrowsInvalidOperationException() + { + // Arrange + var graph = new DirectedWeightedGraph(3); + var vertexA = graph.AddVertex("A"); + var vertexB = graph.AddVertex("B"); + var vertexC = graph.AddVertex("C"); + + graph.AddEdge(vertexA, vertexB, 1); + graph.AddEdge(vertexB, vertexC, 1); + graph.AddEdge(vertexC, vertexA, 1); // Creates a cycle + + var topologicalSort = new TopologicalSort(); + + // Act & Assert + Assert.Throws(() => topologicalSort.SortKahn(graph)); + } + + /// + /// Test topological sort on a single vertex graph. + /// Graph: A (no edges). + /// Expected order: [A]. + /// + [Test] + public void Sort_SingleVertexGraph_ReturnsSingleVertex() + { + // Arrange + var graph = new DirectedWeightedGraph(1); + var vertexA = graph.AddVertex("A"); + + var topologicalSort = new TopologicalSort(); + + // Act + var result = topologicalSort.Sort(graph); + + // Assert + Assert.That(result.Count, Is.EqualTo(1)); + Assert.That(result[0], Is.EqualTo(vertexA)); + } + + /// + /// Test Kahn's algorithm on a single vertex graph. + /// Graph: A (no edges). + /// Expected order: [A]. + /// + [Test] + public void SortKahn_SingleVertexGraph_ReturnsSingleVertex() + { + // Arrange + var graph = new DirectedWeightedGraph(1); + var vertexA = graph.AddVertex("A"); + + var topologicalSort = new TopologicalSort(); + + // Act + var result = topologicalSort.SortKahn(graph); + + // Assert + Assert.That(result.Count, Is.EqualTo(1)); + Assert.That(result[0], Is.EqualTo(vertexA)); + } + + /// + /// Test topological sort on a disconnected DAG. + /// Graph: A → B (component 1) + /// C → D (component 2) + /// Valid orderings: [A, B, C, D], [A, C, B, D], [C, A, B, D], [C, D, A, B], etc. + /// We verify that A comes before B and C comes before D. + /// + [Test] + public void Sort_DisconnectedDAG_ReturnsValidOrder() + { + // Arrange + var graph = new DirectedWeightedGraph(4); + var vertexA = graph.AddVertex("A"); + var vertexB = graph.AddVertex("B"); + var vertexC = graph.AddVertex("C"); + var vertexD = graph.AddVertex("D"); + + graph.AddEdge(vertexA, vertexB, 1); + graph.AddEdge(vertexC, vertexD, 1); + + var topologicalSort = new TopologicalSort(); + + // Act + var result = topologicalSort.Sort(graph); + + // Assert + Assert.That(result.Count, Is.EqualTo(4)); + + // A should come before B + var indexA = result.IndexOf(vertexA); + var indexB = result.IndexOf(vertexB); + Assert.That(indexB, Is.GreaterThan(indexA)); + + // C should come before D + var indexC = result.IndexOf(vertexC); + var indexD = result.IndexOf(vertexD); + Assert.That(indexD, Is.GreaterThan(indexC)); + } + + /// + /// Test Kahn's algorithm on a disconnected DAG. + /// Graph: A → B (component 1) + /// C → D (component 2) + /// Valid orderings: [A, B, C, D], [A, C, B, D], [C, A, B, D], [C, D, A, B], etc. + /// We verify that A comes before B and C comes before D. + /// + [Test] + public void SortKahn_DisconnectedDAG_ReturnsValidOrder() + { + // Arrange + var graph = new DirectedWeightedGraph(4); + var vertexA = graph.AddVertex("A"); + var vertexB = graph.AddVertex("B"); + var vertexC = graph.AddVertex("C"); + var vertexD = graph.AddVertex("D"); + + graph.AddEdge(vertexA, vertexB, 1); + graph.AddEdge(vertexC, vertexD, 1); + + var topologicalSort = new TopologicalSort(); + + // Act + var result = topologicalSort.SortKahn(graph); + + // Assert + Assert.That(result.Count, Is.EqualTo(4)); + + // A should come before B + var indexA = result.IndexOf(vertexA); + var indexB = result.IndexOf(vertexB); + Assert.That(indexB, Is.GreaterThan(indexA)); + + // C should come before D + var indexC = result.IndexOf(vertexC); + var indexD = result.IndexOf(vertexD); + Assert.That(indexD, Is.GreaterThan(indexC)); + } + + /// + /// Test topological sort on a real-world scenario: course prerequisites. + /// Graph represents course dependencies: + /// - Intro to CS (A) is a prerequisite for Data Structures (B) and Algorithms (C). + /// - Data Structures (B) is a prerequisite for Advanced Algorithms (D). + /// - Algorithms (C) is a prerequisite for Advanced Algorithms (D). + /// Expected: A must come first, D must come last. + /// + [Test] + public void Sort_CoursePrerequisitesScenario_ReturnsValidOrder() + { + // Arrange + var graph = new DirectedWeightedGraph(4); + var introCS = graph.AddVertex("Intro to CS"); + var dataStructures = graph.AddVertex("Data Structures"); + var algorithms = graph.AddVertex("Algorithms"); + var advancedAlgorithms = graph.AddVertex("Advanced Algorithms"); + + graph.AddEdge(introCS, dataStructures, 1); + graph.AddEdge(introCS, algorithms, 1); + graph.AddEdge(dataStructures, advancedAlgorithms, 1); + graph.AddEdge(algorithms, advancedAlgorithms, 1); + + var topologicalSort = new TopologicalSort(); + + // Act + var result = topologicalSort.Sort(graph); + + // Assert + Assert.That(result.Count, Is.EqualTo(4)); + Assert.That(result[0], Is.EqualTo(introCS)); // Must take Intro to CS first + Assert.That(result[3], Is.EqualTo(advancedAlgorithms)); // Advanced Algorithms must be last + + // Data Structures and Algorithms should be in the middle + var indexDS = result.IndexOf(dataStructures); + var indexAlgo = result.IndexOf(algorithms); + + Assert.That(indexDS, Is.GreaterThan(0)); + Assert.That(indexDS, Is.LessThan(3)); + Assert.That(indexAlgo, Is.GreaterThan(0)); + Assert.That(indexAlgo, Is.LessThan(3)); + } + + /// + /// Test Kahn's algorithm on a real-world scenario: course prerequisites. + /// Graph represents course dependencies: + /// - Intro to CS (A) is a prerequisite for Data Structures (B) and Algorithms (C). + /// - Data Structures (B) is a prerequisite for Advanced Algorithms (D). + /// - Algorithms (C) is a prerequisite for Advanced Algorithms (D). + /// Expected: A must come first, D must come last. + /// + [Test] + public void SortKahn_CoursePrerequisitesScenario_ReturnsValidOrder() + { + // Arrange + var graph = new DirectedWeightedGraph(4); + var introCS = graph.AddVertex("Intro to CS"); + var dataStructures = graph.AddVertex("Data Structures"); + var algorithms = graph.AddVertex("Algorithms"); + var advancedAlgorithms = graph.AddVertex("Advanced Algorithms"); + + graph.AddEdge(introCS, dataStructures, 1); + graph.AddEdge(introCS, algorithms, 1); + graph.AddEdge(dataStructures, advancedAlgorithms, 1); + graph.AddEdge(algorithms, advancedAlgorithms, 1); + + var topologicalSort = new TopologicalSort(); + + // Act + var result = topologicalSort.SortKahn(graph); + + // Assert + Assert.That(result.Count, Is.EqualTo(4)); + Assert.That(result[0], Is.EqualTo(introCS)); // Must take Intro to CS first + Assert.That(result[3], Is.EqualTo(advancedAlgorithms)); // Advanced Algorithms must be last + + // Data Structures and Algorithms should be in the middle + var indexDS = result.IndexOf(dataStructures); + var indexAlgo = result.IndexOf(algorithms); + + Assert.That(indexDS, Is.GreaterThan(0)); + Assert.That(indexDS, Is.LessThan(3)); + Assert.That(indexAlgo, Is.GreaterThan(0)); + Assert.That(indexAlgo, Is.LessThan(3)); + } + + /// + /// Test topological sort with integer vertices. + /// Graph: 1 → 2 → 3. + /// Expected order: [1, 2, 3]. + /// + [Test] + public void Sort_IntegerVertices_ReturnsCorrectOrder() + { + // Arrange + var graph = new DirectedWeightedGraph(3); + var vertex1 = graph.AddVertex(1); + var vertex2 = graph.AddVertex(2); + var vertex3 = graph.AddVertex(3); + + graph.AddEdge(vertex1, vertex2, 1); + graph.AddEdge(vertex2, vertex3, 1); + + var topologicalSort = new TopologicalSort(); + + // Act + var result = topologicalSort.Sort(graph); + + // Assert + Assert.That(result.Count, Is.EqualTo(3)); + Assert.That(result[0], Is.EqualTo(vertex1)); + Assert.That(result[1], Is.EqualTo(vertex2)); + Assert.That(result[2], Is.EqualTo(vertex3)); + } + + /// + /// Test Kahn's algorithm with integer vertices. + /// Graph: 1 → 2 → 3. + /// Expected order: [1, 2, 3]. + /// + [Test] + public void SortKahn_IntegerVertices_ReturnsCorrectOrder() + { + // Arrange + var graph = new DirectedWeightedGraph(3); + var vertex1 = graph.AddVertex(1); + var vertex2 = graph.AddVertex(2); + var vertex3 = graph.AddVertex(3); + + graph.AddEdge(vertex1, vertex2, 1); + graph.AddEdge(vertex2, vertex3, 1); + + var topologicalSort = new TopologicalSort(); + + // Act + var result = topologicalSort.SortKahn(graph); + + // Assert + Assert.That(result.Count, Is.EqualTo(3)); + Assert.That(result[0], Is.EqualTo(vertex1)); + Assert.That(result[1], Is.EqualTo(vertex2)); + Assert.That(result[2], Is.EqualTo(vertex3)); + } + + /// + /// Test topological sort on a graph with self-loop (cycle). + /// Graph: A → A (self-loop). + /// Should throw InvalidOperationException. + /// + [Test] + public void Sort_GraphWithSelfLoop_ThrowsInvalidOperationException() + { + // Arrange + var graph = new DirectedWeightedGraph(1); + var vertexA = graph.AddVertex("A"); + + graph.AddEdge(vertexA, vertexA, 1); // Self-loop + + var topologicalSort = new TopologicalSort(); + + // Act & Assert + Assert.Throws(() => topologicalSort.Sort(graph)); + } + + /// + /// Test Kahn's algorithm on a graph with self-loop (cycle). + /// Graph: A → A (self-loop). + /// Should throw InvalidOperationException. + /// + [Test] + public void SortKahn_GraphWithSelfLoop_ThrowsInvalidOperationException() + { + // Arrange + var graph = new DirectedWeightedGraph(1); + var vertexA = graph.AddVertex("A"); + + graph.AddEdge(vertexA, vertexA, 1); // Self-loop + + var topologicalSort = new TopologicalSort(); + + // Act & Assert + Assert.Throws(() => topologicalSort.SortKahn(graph)); + } +} diff --git a/Algorithms/Graph/TopologicalSort.cs b/Algorithms/Graph/TopologicalSort.cs new file mode 100644 index 00000000..dca71cdf --- /dev/null +++ b/Algorithms/Graph/TopologicalSort.cs @@ -0,0 +1,290 @@ +using DataStructures.Graph; + +namespace Algorithms.Graph; + +/// +/// Topological Sort is a linear ordering of vertices in a Directed Acyclic Graph (DAG) +/// such that for every directed edge (u, v), vertex u comes before vertex v in the ordering. +/// +/// KEY CONCEPTS: +/// 1. Only applicable to Directed Acyclic Graphs (DAGs) - graphs with no cycles. +/// 2. A DAG can have multiple valid topological orderings. +/// 3. Used in dependency resolution, task scheduling, build systems, and course prerequisites. +/// +/// ALGORITHM APPROACHES: +/// 1. DFS-based (Depth-First Search): Uses post-order traversal and reverses the result. +/// 2. Kahn's Algorithm: Uses in-degree counting and processes vertices with zero in-degree. +/// +/// TIME COMPLEXITY: O(V + E) where V is vertices and E is edges. +/// SPACE COMPLEXITY: O(V) for the visited set and result stack. +/// +/// Reference: "Introduction to Algorithms" (CLRS) by Cormen, Leiserson, Rivest, and Stein. +/// Also covered in "Algorithm Design Manual" by Steven Skiena. +/// +/// Vertex data type. +public class TopologicalSort where T : IComparable +{ + /// + /// Performs topological sort on a directed acyclic graph using DFS-based approach. + /// + /// ALGORITHM STEPS (DFS-based approach): + /// 1. Initialize a visited set to track processed vertices. + /// 2. Initialize a stack to store the topological ordering. + /// 3. For each unvisited vertex in the graph: + /// a) Perform DFS from that vertex. + /// b) After visiting all descendants, push the vertex to the stack. + /// 4. The stack now contains vertices in reverse topological order. + /// 5. Pop all vertices from the stack to get the topological ordering. + /// + /// WHY IT WORKS: + /// - In DFS, we push a vertex to the stack only after visiting all its descendants. + /// - This ensures that all vertices that depend on the current vertex are processed first. + /// - Reversing this order gives us the topological sort. + /// + /// EXAMPLE: + /// Graph: A → B → C + /// A → D + /// D → C + /// Valid topological orderings: [A, B, D, C] or [A, D, B, C]. + /// + /// USE CASES: + /// - Build systems (compile dependencies). + /// - Task scheduling with dependencies. + /// - Course prerequisite ordering. + /// - Package dependency resolution. + /// + /// The directed acyclic graph to sort. + /// A list of vertices in topological order. + /// + /// Thrown when the graph contains a cycle (not a DAG). + /// + public List> Sort(IDirectedWeightedGraph graph) + { + // Stack to store vertices in reverse topological order. + // We use a stack because DFS naturally gives us reverse topological order. + var stack = new Stack>(); + + // Track visited vertices to avoid reprocessing and detect cycles. + var visited = new HashSet>(); + + // Track vertices currently in the recursion stack to detect cycles. + // If we encounter a vertex that's in the recursion stack, we have a cycle. + var recursionStack = new HashSet>(); + + // Process all vertices in the graph. + // We need to iterate through all vertices because the graph might be disconnected. + for (int i = 0; i < graph.Count; i++) + { + var vertex = graph.Vertices[i]; + + // Skip null vertices (shouldn't happen in a well-formed graph). + if (vertex == null) + { + continue; + } + + // If vertex hasn't been visited, perform DFS from it. + if (!visited.Contains(vertex)) + { + DfsTopologicalSort(graph, vertex, visited, recursionStack, stack); + } + } + + // Convert stack to list. The stack contains vertices in reverse topological order, + // so we need to reverse it to get the correct topological ordering. + var result = new List>(stack.Count); + while (stack.Count > 0) + { + result.Add(stack.Pop()); + } + + return result; + } + + /// + /// Performs topological sort using Kahn's Algorithm (BFS-based approach). + /// + /// ALGORITHM STEPS (Kahn's Algorithm): + /// 1. Calculate in-degree (number of incoming edges) for each vertex. + /// 2. Add all vertices with in-degree 0 to a queue. + /// 3. While the queue is not empty: + /// a) Remove a vertex from the queue and add it to the result. + /// b) For each neighbor of this vertex: + /// - Decrease its in-degree by 1. + /// - If in-degree becomes 0, add it to the queue. + /// 4. If all vertices are processed, return the result. + /// 5. If not all vertices are processed, the graph has a cycle. + /// + /// WHY IT WORKS: + /// - Vertices with in-degree 0 have no dependencies and can be processed first. + /// - After processing a vertex, we "remove" its outgoing edges by decreasing + /// the in-degree of its neighbors. + /// - This gradually reveals more vertices with in-degree 0. + /// + /// ADVANTAGES OVER DFS: + /// - More intuitive for understanding dependencies. + /// - Easier to detect cycles (if not all vertices are processed). + /// - Better for parallel processing scenarios. + /// + /// The directed acyclic graph to sort. + /// A list of vertices in topological order. + /// + /// Thrown when the graph contains a cycle (not a DAG). + /// + public List> SortKahn(IDirectedWeightedGraph graph) + { + // Calculate in-degree for each vertex. + // In-degree is the number of incoming edges to a vertex. + var inDegree = new Dictionary, int>(); + + // Initialize in-degree for all vertices to 0. + for (int i = 0; i < graph.Count; i++) + { + var vertex = graph.Vertices[i]; + if (vertex != null) + { + inDegree[vertex] = 0; + } + } + + // Calculate actual in-degrees by examining all edges. + for (int i = 0; i < graph.Count; i++) + { + var vertex = graph.Vertices[i]; + if (vertex == null) + { + continue; + } + + // For each neighbor, increment its in-degree. + foreach (var neighbor in graph.GetNeighbors(vertex)) + { + if (neighbor != null) + { + inDegree[neighbor]++; + } + } + } + + // Queue to process vertices with in-degree 0. + // These vertices have no dependencies and can be processed first. + var queue = new Queue>(); + + // Add all vertices with in-degree 0 to the queue. + foreach (var kvp in inDegree) + { + if (kvp.Value == 0) + { + queue.Enqueue(kvp.Key); + } + } + + // Result list to store the topological ordering. + var result = new List>(); + + // Process vertices in topological order. + while (queue.Count > 0) + { + // Remove a vertex with in-degree 0. + var vertex = queue.Dequeue(); + result.Add(vertex); + + // For each neighbor, decrease its in-degree. + // This simulates "removing" the edge from vertex to neighbor. + foreach (var neighbor in graph.GetNeighbors(vertex)) + { + if (neighbor == null) + { + continue; + } + + inDegree[neighbor]--; + + // If in-degree becomes 0, the neighbor can now be processed. + if (inDegree[neighbor] == 0) + { + queue.Enqueue(neighbor); + } + } + } + + // CYCLE DETECTION: + // If we haven't processed all vertices, there must be a cycle. + // Vertices in a cycle will never have in-degree 0 because they depend on each other. + if (result.Count != graph.Count) + { + throw new InvalidOperationException( + "Graph contains a cycle. Topological sort is only possible for Directed Acyclic Graphs (DAGs)."); + } + + return result; + } + + /// + /// Helper method for DFS-based topological sort. + /// Recursively visits vertices and adds them to the stack in post-order. + /// + /// POST-ORDER TRAVERSAL: + /// - Visit all descendants first. + /// - Then process the current vertex. + /// - This ensures dependencies are processed before dependents. + /// + /// CYCLE DETECTION: + /// - We maintain a recursion stack to track the current DFS path. + /// - If we encounter a vertex that's already in the recursion stack, + /// we've found a back edge, indicating a cycle. + /// + /// The graph being sorted. + /// The current vertex being processed. + /// Set of all visited vertices. + /// Set of vertices in the current DFS path. + /// Stack to store vertices in reverse topological order. + /// + /// Thrown when a cycle is detected. + /// + private void DfsTopologicalSort( + IDirectedWeightedGraph graph, + Vertex vertex, + HashSet> visited, + HashSet> recursionStack, + Stack> stack) + { + // CYCLE DETECTION: + // If the vertex is in the recursion stack, we've encountered it again + // in the current DFS path, which means there's a cycle. + if (recursionStack.Contains(vertex)) + { + throw new InvalidOperationException( + $"Graph contains a cycle involving vertex: {vertex}. " + + "Topological sort is only possible for Directed Acyclic Graphs (DAGs)."); + } + + // If already visited, no need to process again. + if (visited.Contains(vertex)) + { + return; + } + + // Mark vertex as visited and add to recursion stack. + visited.Add(vertex); + recursionStack.Add(vertex); + + // Recursively visit all neighbors (descendants). + // This ensures all dependencies are processed first. + foreach (var neighbor in graph.GetNeighbors(vertex)) + { + if (neighbor != null) + { + DfsTopologicalSort(graph, neighbor, visited, recursionStack, stack); + } + } + + // Remove from recursion stack as we're done with this DFS path. + recursionStack.Remove(vertex); + + // POST-ORDER: Add vertex to stack after visiting all descendants. + // This ensures that all vertices that depend on the current vertex + // are already in the stack (deeper in the stack). + stack.Push(vertex); + } +} From 2e184b552dedbf5de26c042fb5b5e8dab64fe713 Mon Sep 17 00:00:00 2001 From: codomposer Date: Mon, 10 Nov 2025 20:13:55 -0500 Subject: [PATCH 2/3] update link in readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 93ea4e25..26fe0749 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ find more than one implementation for the same objective but using different alg * [Dijkstra Shortest Path](./Algorithms/Graph/Dijkstra/DijkstraAlgorithm.cs) * [FloydWarshall](./Algorithms/Graph/FloydWarshall.cs) * [Kosaraju](./Algorithms/Graph/Kosaraju.cs) + * [Topological Sort](./Algorithms/Graph/TopologicalSort.cs) * [Knapsack problem](./Algorithms/Knapsack) * [Naive solver](./Algorithms/Knapsack/NaiveKnapsackSolver.cs) * [Dynamic Programming solver](./Algorithms/Knapsack/DynamicProgrammingKnapsackSolver.cs) From 19b9a6467b254afb1d1d1b4fa4e60ef8f732e1e0 Mon Sep 17 00:00:00 2001 From: codomposer Date: Mon, 10 Nov 2025 20:30:51 -0500 Subject: [PATCH 3/3] fix: reduce the complexity of SortKahn method --- Algorithms/Graph/TopologicalSort.cs | 131 ++++++++++++++++++++++------ 1 file changed, 102 insertions(+), 29 deletions(-) diff --git a/Algorithms/Graph/TopologicalSort.cs b/Algorithms/Graph/TopologicalSort.cs index dca71cdf..ac542ce6 100644 --- a/Algorithms/Graph/TopologicalSort.cs +++ b/Algorithms/Graph/TopologicalSort.cs @@ -134,7 +134,28 @@ public List> Sort(IDirectedWeightedGraph graph) public List> SortKahn(IDirectedWeightedGraph graph) { // Calculate in-degree for each vertex. - // In-degree is the number of incoming edges to a vertex. + var inDegree = CalculateInDegrees(graph); + + // Queue to process vertices with in-degree 0. + var queue = InitializeQueueWithZeroInDegreeVertices(inDegree); + + // Process vertices in topological order. + var result = ProcessVerticesInTopologicalOrder(graph, inDegree, queue); + + // Verify all vertices were processed (no cycles). + ValidateNoCycles(graph, result); + + return result; + } + + /// + /// Calculates the in-degree for each vertex in the graph. + /// In-degree is the number of incoming edges to a vertex. + /// + /// The graph to analyze. + /// Dictionary mapping each vertex to its in-degree. + private Dictionary, int> CalculateInDegrees(IDirectedWeightedGraph graph) + { var inDegree = new Dictionary, int>(); // Initialize in-degree for all vertices to 0. @@ -151,26 +172,45 @@ public List> SortKahn(IDirectedWeightedGraph graph) for (int i = 0; i < graph.Count; i++) { var vertex = graph.Vertices[i]; - if (vertex == null) + if (vertex != null) { - continue; + IncrementNeighborInDegrees(graph, vertex, inDegree); } + } + + return inDegree; + } - // For each neighbor, increment its in-degree. - foreach (var neighbor in graph.GetNeighbors(vertex)) + /// + /// Increments the in-degree for all neighbors of a given vertex. + /// + /// The graph containing the vertices. + /// The vertex whose neighbors' in-degrees should be incremented. + /// Dictionary tracking in-degrees. + private void IncrementNeighborInDegrees( + IDirectedWeightedGraph graph, + Vertex vertex, + Dictionary, int> inDegree) + { + foreach (var neighbor in graph.GetNeighbors(vertex)) + { + if (neighbor != null) { - if (neighbor != null) - { - inDegree[neighbor]++; - } + inDegree[neighbor]++; } } + } - // Queue to process vertices with in-degree 0. - // These vertices have no dependencies and can be processed first. + /// + /// Initializes a queue with all vertices that have in-degree 0. + /// These vertices have no dependencies and can be processed first. + /// + /// Dictionary mapping vertices to their in-degrees. + /// Queue containing all vertices with in-degree 0. + private Queue> InitializeQueueWithZeroInDegreeVertices(Dictionary, int> inDegree) + { var queue = new Queue>(); - // Add all vertices with in-degree 0 to the queue. foreach (var kvp in inDegree) { if (kvp.Value == 0) @@ -179,45 +219,78 @@ public List> SortKahn(IDirectedWeightedGraph graph) } } - // Result list to store the topological ordering. + return queue; + } + + /// + /// Processes vertices in topological order using Kahn's algorithm. + /// Dequeues vertices with in-degree 0 and decreases in-degrees of their neighbors. + /// + /// The graph being sorted. + /// Dictionary tracking in-degrees. + /// Queue of vertices with in-degree 0. + /// List of vertices in topological order. + private List> ProcessVerticesInTopologicalOrder( + IDirectedWeightedGraph graph, + Dictionary, int> inDegree, + Queue> queue) + { var result = new List>(); - // Process vertices in topological order. while (queue.Count > 0) { - // Remove a vertex with in-degree 0. var vertex = queue.Dequeue(); result.Add(vertex); - // For each neighbor, decrease its in-degree. - // This simulates "removing" the edge from vertex to neighbor. - foreach (var neighbor in graph.GetNeighbors(vertex)) - { - if (neighbor == null) - { - continue; - } + ProcessNeighbors(graph, vertex, inDegree, queue); + } + + return result; + } + /// + /// Processes neighbors of a vertex by decreasing their in-degrees. + /// If a neighbor's in-degree becomes 0, it's added to the queue. + /// + /// The graph being sorted. + /// The vertex whose neighbors are being processed. + /// Dictionary tracking in-degrees. + /// Queue of vertices with in-degree 0. + private void ProcessNeighbors( + IDirectedWeightedGraph graph, + Vertex vertex, + Dictionary, int> inDegree, + Queue> queue) + { + foreach (var neighbor in graph.GetNeighbors(vertex)) + { + if (neighbor != null) + { inDegree[neighbor]--; - // If in-degree becomes 0, the neighbor can now be processed. if (inDegree[neighbor] == 0) { queue.Enqueue(neighbor); } } } + } - // CYCLE DETECTION: - // If we haven't processed all vertices, there must be a cycle. - // Vertices in a cycle will never have in-degree 0 because they depend on each other. + /// + /// Validates that all vertices were processed, ensuring no cycles exist. + /// + /// The graph being sorted. + /// The list of processed vertices. + /// + /// Thrown when not all vertices were processed (cycle detected). + /// + private void ValidateNoCycles(IDirectedWeightedGraph graph, List> result) + { if (result.Count != graph.Count) { throw new InvalidOperationException( "Graph contains a cycle. Topological sort is only possible for Directed Acyclic Graphs (DAGs)."); } - - return result; } ///