Skip to content

Commit faa1c13

Browse files
committed
ruby: minimal graph, topological sort
1 parent b622fe7 commit faa1c13

File tree

4 files changed

+155
-1
lines changed

4 files changed

+155
-1
lines changed

ruby/lib/dsa.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
require_relative "dsa/binary_tree"
77
require_relative "dsa/trie"
88
require_relative "dsa/array_list"
9+
require_relative "dsa/graph"
910

1011
# Data structures and algorithms practice.
1112
module DSA

ruby/lib/dsa/graph.rb

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
module DSA
2+
# A graph, implemented as an adjacency list.
3+
#
4+
class Graph
5+
class Vertex
6+
# @return [T]
7+
attr_accessor :value
8+
9+
# @param value [T]
10+
def initialize value:
11+
@value = value
12+
end
13+
end
14+
15+
def initialize
16+
@adjacency_list = {}
17+
@in_degrees = {}
18+
end
19+
20+
attr_reader :adjacency_list
21+
22+
def add_vertex value:
23+
vertex = Vertex.new(value:)
24+
@adjacency_list[vertex] = SinglyLinkedList.new
25+
@in_degrees[vertex] ||= 0
26+
vertex
27+
end
28+
29+
def add_edge from:, to:
30+
# TODO: ensure vertices are already present in the graph
31+
@adjacency_list.fetch(from).prepend(to)
32+
@in_degrees[to] += 1
33+
end
34+
35+
# Kahn’s algorithm
36+
#
37+
# - Time: O(V + E)
38+
# - Space: O(V)
39+
#
40+
def topological_sort
41+
sources = DSA::Queue.new
42+
result = []
43+
44+
@in_degrees.select { |k, v| v.zero? }.keys.each do |source|
45+
sources.enqueue source
46+
end
47+
48+
until sources.empty?
49+
source = sources.dequeue
50+
result << source
51+
52+
children = @adjacency_list[source]
53+
54+
children.each do |node|
55+
child = node.value
56+
57+
@in_degrees[child] -= 1
58+
59+
if @in_degrees[child].zero?
60+
sources.enqueue child
61+
end
62+
end
63+
end
64+
65+
if result.size < @adjacency_list.keys.size
66+
raise ArgumentError, "graph cannot contain cycles"
67+
end
68+
69+
result
70+
end
71+
end
72+
end

ruby/spec/dsa/graph_spec.rb

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
RSpec.describe DSA::Graph do
2+
describe "#add_vertex" do
3+
it "creates a new vertex" do
4+
graph = described_class.new
5+
6+
vertex = graph.add_vertex(value: 42)
7+
8+
expect(vertex).to be_a described_class::Vertex
9+
expect(vertex.value).to eq 42
10+
end
11+
end
12+
13+
describe "#add_edge" do
14+
it "creates a new edge" do
15+
graph = described_class.new
16+
17+
one = graph.add_vertex(value: 1)
18+
two = graph.add_vertex(value: 2)
19+
20+
graph.add_edge(from: one, to: two)
21+
22+
edges = graph.adjacency_list[one]
23+
expect(edges).to be_a DSA::SinglyLinkedList
24+
expect(edges.length).to eq 1
25+
expect(edges[0].value).to be_a described_class::Vertex
26+
expect(edges[0].value.value).to eq 2
27+
end
28+
end
29+
30+
describe "#topological_sort" do
31+
it "returns a sorted list of vertices" do
32+
graph = described_class.new
33+
34+
cook_veggies = graph.add_vertex(value: :cook_veggies)
35+
cook_meat = graph.add_vertex(value: :cook_meat)
36+
prep_veggies = graph.add_vertex(value: :prep_veggies)
37+
prep_meat = graph.add_vertex(value: :prep_meat)
38+
cook_rice = graph.add_vertex(value: :cook_rice)
39+
serve_meal = graph.add_vertex(value: :serve_meal)
40+
buy_food = graph.add_vertex(value: :buy_food)
41+
42+
graph.add_edge(from: buy_food, to: prep_veggies)
43+
graph.add_edge(from: buy_food, to: prep_meat)
44+
graph.add_edge(from: buy_food, to: cook_rice)
45+
46+
graph.add_edge(from: prep_veggies, to: cook_veggies)
47+
graph.add_edge(from: prep_meat, to: cook_meat)
48+
49+
graph.add_edge(from: cook_veggies, to: serve_meal)
50+
graph.add_edge(from: cook_meat, to: serve_meal)
51+
graph.add_edge(from: cook_rice, to: serve_meal)
52+
53+
result = graph.topological_sort
54+
55+
expect(result).to be_a Array
56+
expect(result[0]).to eq buy_food
57+
expect(result[1]).to eq cook_rice
58+
expect(result[2]).to eq prep_meat
59+
expect(result[3]).to eq prep_veggies
60+
expect(result[4]).to eq cook_meat
61+
expect(result[5]).to eq cook_veggies
62+
expect(result[6]).to eq serve_meal
63+
end
64+
65+
it "errors if graph contains a cycle" do
66+
graph = described_class.new
67+
68+
one = graph.add_vertex(value: 1)
69+
two = graph.add_vertex(value: 2)
70+
three = graph.add_vertex(value: 3)
71+
72+
graph.add_edge(from: one, to: two)
73+
graph.add_edge(from: two, to: three)
74+
graph.add_edge(from: three, to: one)
75+
76+
expect { graph.topological_sort }
77+
.to raise_error(ArgumentError, "graph cannot contain cycles")
78+
end
79+
end
80+
end

ruby/spec/dsa_spec.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
it { is_expected.to eq("0.0.0") }
66
end
77

8-
it "has the correct gemspec info" do
8+
it "has the correct gemspec info" do # rubocop:disable RSpec/ExampleLength
99
path = File.expand_path("../../dsa.gemspec", __FILE__)
1010
gemspec = Gem::Specification.load path
1111

@@ -20,6 +20,7 @@
2020
"lib/dsa/binary_tree.rb",
2121
"lib/dsa/deque.rb",
2222
"lib/dsa/doubly_linked_list.rb",
23+
"lib/dsa/graph.rb",
2324
"lib/dsa/queue.rb",
2425
"lib/dsa/singly_linked_list.rb",
2526
"lib/dsa/stack.rb",

0 commit comments

Comments
 (0)