diff --git a/data_structures/graphs/topological_sort.rb b/data_structures/graphs/topological_sort.rb new file mode 100644 index 0000000..88955e4 --- /dev/null +++ b/data_structures/graphs/topological_sort.rb @@ -0,0 +1,37 @@ +require 'set' + +## +# This class aims to provide topological sorting capabilities for directed acyclic graphs. +# +# Topological sorting runs in O(|V|), where |V| is the number of graph nodes. + +class TopologicalSorter + attr_reader :graph + + def initialize(graph) + raise ArgumentError, "Topological sort is only applicable to directed graphs!" unless graph.directed + @graph = graph + end + + def topological_sort + @sorted_nodes = [] + @seen = Set[] + @visited = Set[] + for node in graph.nodes + dfs_visit(node) + end + @sorted_nodes + end + + private + def dfs_visit(node) + return if @visited.include?(node) + raise ArgumentError, "Cycle in graph detected on node #{node}!" if @seen.include?(node) + @seen.add(node) + for neighbor in graph.neighbors(node) + dfs_visit(neighbor) + end + @visited.add(node) + @sorted_nodes.unshift(node) + end +end diff --git a/data_structures/graphs/topological_sort_test.rb b/data_structures/graphs/topological_sort_test.rb new file mode 100644 index 0000000..01472da --- /dev/null +++ b/data_structures/graphs/topological_sort_test.rb @@ -0,0 +1,52 @@ +require 'minitest/autorun' +require_relative 'topological_sort' +require_relative 'unweighted_graph' + +class TestTopologicalSort < Minitest::Test + def test_topological_sort_returns_valid_order_for_acyclic_graph + wardrobe_items = [:underwear, :trousers, :belt, :shirt, :tie, :jacket, :socks, :shoes, :watch] + wardrobe_graph = UnweightedGraph.new(nodes: wardrobe_items, directed: true) + wardrobe_graph.add_edge(:underwear, :trousers) + wardrobe_graph.add_edge(:underwear, :shoes) + wardrobe_graph.add_edge(:socks, :shoes) + wardrobe_graph.add_edge(:trousers, :shoes) + wardrobe_graph.add_edge(:trousers, :belt) + wardrobe_graph.add_edge(:shirt, :belt) + wardrobe_graph.add_edge(:belt, :jacket) + wardrobe_graph.add_edge(:shirt, :tie) + wardrobe_graph.add_edge(:tie, :jacket) + + sorted_items = TopologicalSorter.new(wardrobe_graph).topological_sort + + assert sorted_items.index(:underwear) < sorted_items.index(:trousers) + assert sorted_items.index(:underwear) < sorted_items.index(:shoes) + assert sorted_items.index(:socks) < sorted_items.index(:shoes) + assert sorted_items.index(:trousers) < sorted_items.index(:shoes) + assert sorted_items.index(:trousers) < sorted_items.index(:belt) + assert sorted_items.index(:shirt) < sorted_items.index(:belt) + assert sorted_items.index(:belt) < sorted_items.index(:jacket) + assert sorted_items.index(:shirt) < sorted_items.index(:tie) + assert sorted_items.index(:tie) < sorted_items.index(:jacket) + end + + def test_topological_sort_raises_exception_for_undirected_graph + nodes = [:u, :v] + graph = UnweightedGraph.new(nodes: nodes, directed: false) + graph.add_edge(:u, :v) + + assert_raises ArgumentError do + TopologicalSorter.new(graph).topological_sort + end + end + + def test_topological_sort_raises_exception_for_cyclic_graph + nodes = [:u, :v] + graph = UnweightedGraph.new(nodes: nodes, directed: true) + graph.add_edge(:u, :v) + graph.add_edge(:v, :u) + + assert_raises ArgumentError do + TopologicalSorter.new(graph).topological_sort + end + end +end