Skip to content

Commit 774a964

Browse files
authored
Merge pull request #207 from aparibocci/feature/graph_bfs
Implementing BFS for unweighted graphs
2 parents 450ba13 + 85a1175 commit 774a964

File tree

4 files changed

+156
-2
lines changed

4 files changed

+156
-2
lines changed

Diff for: data_structures/graphs/bfs.rb

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
require 'set'
2+
3+
##
4+
# This class represents the result of a breadth-first search performed on an unweighted graph.
5+
#
6+
# It exposes:
7+
# - the set of visited nodes
8+
# - a hash of distances by node from the search root node
9+
# (only for visited nodes, 0 for the search root node);
10+
# - a hash of parent nodes by node
11+
# (only for visited nodes, nil for the search root node).
12+
13+
class GraphBfsResult
14+
attr_reader :visited
15+
attr_reader :parents
16+
attr_reader :distances
17+
18+
def initialize(visited, parents, distances)
19+
@visited = visited
20+
@parents = parents
21+
@distances = distances
22+
end
23+
end
24+
25+
##
26+
# Performs a breadth-first search for the provided graph, starting at the given node.
27+
# Returns the search result (see GraphBfsResult).
28+
# Nodes are consumed using the provided consumers upon being first seen, or being completely visited
29+
# (nothing, by default).
30+
#
31+
# The algorithm has a time complexity of O(|V| + |E|), where:
32+
# - |V| is the number of nodes in the graph;
33+
# - |E| is the number of edges in the graph.
34+
35+
def bfs(graph, start_node, seen_node_consumer: method(:do_nothing_on_node), visited_node_consumer: method(:do_nothing_on_node))
36+
seen = Set[]
37+
visited = Set[]
38+
parents = { start_node => nil }
39+
distances = { start_node => 0 }
40+
41+
seen.add(start_node)
42+
seen_node_consumer.call(start_node)
43+
q = Queue.new
44+
q.push(start_node)
45+
until q.empty?
46+
node = q.pop
47+
for neighbor in graph.neighbors(node)
48+
unless seen.include?(neighbor)
49+
seen.add(neighbor)
50+
distances[neighbor] = distances[node] + 1
51+
parents[neighbor] = node
52+
seen_node_consumer.call(neighbor)
53+
q.push(neighbor)
54+
end
55+
end
56+
visited.add(node)
57+
visited_node_consumer.call(node)
58+
end
59+
60+
GraphBfsResult.new(visited, parents, distances)
61+
end
62+
63+
private
64+
def do_nothing_on_node(node)
65+
end

Diff for: data_structures/graphs/bfs_test.rb

+89
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
require 'minitest/autorun'
2+
require_relative 'bfs'
3+
require_relative 'unweighted_graph'
4+
5+
class TestBfs < Minitest::Test
6+
def test_bfs_visits_single_graph_node
7+
graph = UnweightedGraph.new(nodes: [:u, :v, :w], directed: false)
8+
graph.add_edge(:u, :v)
9+
10+
bfs_result = bfs(graph, :w)
11+
12+
assert bfs_result.visited.to_set == [:w].to_set
13+
assert bfs_result.parents == {
14+
:w => nil
15+
}
16+
assert bfs_result.distances == {
17+
:w => 0
18+
}
19+
end
20+
21+
def test_bfs_visits_graph_fully
22+
graph = UnweightedGraph.new(nodes: [:u, :v, :w, :x], directed: false)
23+
graph.add_edge(:u, :v)
24+
graph.add_edge(:u, :w)
25+
graph.add_edge(:w, :x)
26+
27+
bfs_result = bfs(graph, :u)
28+
29+
assert bfs_result.visited.to_set == [:u, :v, :w, :x].to_set
30+
assert bfs_result.parents == {
31+
:u => nil,
32+
:v => :u,
33+
:w => :u,
34+
:x => :w
35+
}
36+
assert bfs_result.distances == {
37+
:u => 0,
38+
:v => 1,
39+
:w => 1,
40+
:x => 2
41+
}
42+
end
43+
44+
def test_bfs_visits_graph_partially
45+
graph = UnweightedGraph.new(nodes: [:u, :v, :w, :x, :y, :z], directed: false)
46+
graph.add_edge(:u, :v)
47+
graph.add_edge(:w, :x)
48+
graph.add_edge(:x, :y)
49+
graph.add_edge(:y, :z)
50+
51+
bfs_result = bfs(graph, :x)
52+
53+
assert bfs_result.visited.to_set == [:w, :x, :y, :z].to_set
54+
assert bfs_result.parents == {
55+
:w => :x,
56+
:x => nil,
57+
:y => :x,
58+
:z => :y
59+
}
60+
assert bfs_result.distances == {
61+
:w => 1,
62+
:x => 0,
63+
:y => 1,
64+
:z => 2
65+
}
66+
end
67+
68+
def test_bfs_visits_with_seen_node_consumer
69+
graph = UnweightedGraph.new(nodes: [:u, :v, :w], directed: false)
70+
graph.add_edge(:u, :v)
71+
graph.add_edge(:u, :w)
72+
73+
seen_order = []
74+
bfs(graph, :w, seen_node_consumer: ->(node) { seen_order.append(node) })
75+
76+
assert seen_order == [:w, :u, :v]
77+
end
78+
79+
def test_bfs_visits_with_visited_node_consumer
80+
graph = UnweightedGraph.new(nodes: [:u, :v, :w], directed: false)
81+
graph.add_edge(:u, :v)
82+
graph.add_edge(:u, :w)
83+
84+
visited_order = []
85+
bfs(graph, :w, visited_node_consumer: ->(node) { visited_order.append(node) })
86+
87+
assert visited_order == [:w, :u, :v]
88+
end
89+
end

Diff for: data_structures/graphs/unweighted_graph_test.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ def test_add_edge_adds_edge_to_directed_unweighted_graph
7272
assert graph.neighbors(:v).empty?
7373
end
7474

75-
def test_add_edge_adds_edge_to_directed_unweighted_graph
75+
def test_add_edge_adds_edge_to_undirected_unweighted_graph
7676
graph = UnweightedGraph.new(nodes: [:u, :v], directed: false)
7777
graph.add_edge(:u, :v)
7878

Diff for: data_structures/graphs/weighted_graph_test.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ def test_add_edge_adds_edge_to_directed_weighted_graph
7878
assert graph.edges(:v).empty?
7979
end
8080

81-
def test_add_edge_adds_edge_to_directed_weighted_graph
81+
def test_add_edge_adds_edge_to_undirected_weighted_graph
8282
graph = WeightedGraph.new(nodes: [:u, :v], directed: false)
8383
graph.add_edge(:u, :v, 2)
8484

0 commit comments

Comments
 (0)