From 360cba9a6114c6082ecf23d188759a8c8b6c946d Mon Sep 17 00:00:00 2001 From: Amos Paribocci Date: Tue, 23 May 2023 08:37:39 +0200 Subject: [PATCH] Implementing unweighted graph BFS --- data_structures/graphs/bfs.rb | 56 +++++++++++++++++++++++++ data_structures/graphs/bfs_test.rb | 67 ++++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+) create mode 100644 data_structures/graphs/bfs.rb create mode 100644 data_structures/graphs/bfs_test.rb diff --git a/data_structures/graphs/bfs.rb b/data_structures/graphs/bfs.rb new file mode 100644 index 0000000..fcb1b33 --- /dev/null +++ b/data_structures/graphs/bfs.rb @@ -0,0 +1,56 @@ +require 'set' + +## +# This class represents the result of a breadth-first search performed on an unweighted graph. +# +# It exposes: +# - the set of visited nodes +# - a hash of distances by node from the search root node +# (only for visited nodes, 0 for the search root node); +# - a hash of parent nodes by node +# (only for visited nodes, nil for the search root node). + +class GraphBfsResult + attr_reader :visited + attr_reader :parents + attr_reader :distances + + def initialize(visited, parents, distances) + @visited = visited + @parents = parents + @distances = distances + end +end + +## +# Performs a breadth-first search for the provided graph, starting at the given node. +# Returns the search result (see GraphBfsResult). +# +# The algorithm has a time complexity of O(|V| + |E|), where: +# - |V| is the number of nodes in the graph; +# - |E| is the number of edges in the graph. + +def bfs(graph, start_node) + seen = Set[] + visited = Set[] + parents = { start_node => nil } + distances = { start_node => 0 } + + seen.add(start_node) + q = Queue.new + q.push(start_node) + until q.empty? + node = q.pop + for neighbor in graph.neighbors(node) + unless seen.include?(neighbor) + seen.add(neighbor) + distances[neighbor] = distances[node] + 1 + parents[neighbor] = node + q.push(neighbor) + end + end + visited.add(node) + end + + GraphBfsResult.new(visited, parents, distances) +end diff --git a/data_structures/graphs/bfs_test.rb b/data_structures/graphs/bfs_test.rb new file mode 100644 index 0000000..933adab --- /dev/null +++ b/data_structures/graphs/bfs_test.rb @@ -0,0 +1,67 @@ +require 'minitest/autorun' +require_relative 'bfs' +require_relative 'unweighted_graph' + +class TestBfs < Minitest::Test + def test_bfs_visits_single_graph_node + graph = UnweightedGraph.new(nodes: [:u, :v, :w], directed: false) + graph.add_edge(:u, :v) + + bfs_result = bfs(graph, :w) + + assert bfs_result.visited.to_set == [:w].to_set + assert bfs_result.parents == { + :w => nil + } + assert bfs_result.distances == { + :w => 0 + } + end + + def test_bfs_visits_undirected_graph_fully + graph = UnweightedGraph.new(nodes: [:u, :v, :w, :x], directed: false) + graph.add_edge(:u, :v) + graph.add_edge(:u, :w) + graph.add_edge(:w, :x) + + bfs_result = bfs(graph, :u) + + assert bfs_result.visited.to_set == [:u, :v, :w, :x].to_set + assert bfs_result.parents == { + :u => nil, + :v => :u, + :w => :u, + :x => :w + } + assert bfs_result.distances == { + :u => 0, + :v => 1, + :w => 1, + :x => 2 + } + end + + def test_bfs_visits_undirected_graph_partially + graph = UnweightedGraph.new(nodes: [:u, :v, :w, :x, :y, :z], directed: false) + graph.add_edge(:u, :v) + graph.add_edge(:w, :x) + graph.add_edge(:x, :y) + graph.add_edge(:y, :z) + + bfs_result = bfs(graph, :x) + + assert bfs_result.visited.to_set == [:w, :x, :y, :z].to_set + assert bfs_result.parents == { + :w => :x, + :x => nil, + :y => :x, + :z => :y + } + assert bfs_result.distances == { + :w => 1, + :x => 0, + :y => 1, + :z => 2 + } + end +end