Lab 8 - Graph Implementation
In this lab session we will develop a simple adjacency list based weighted graph data structure. We will also implement BFS and DFS for the graph using the visitor pattern.
Graph
// AdjacencyListGraph.h #pragma once #include <iterator> #include <queue> #include <stack> #include <vector> namespace csc280 { template <typename V, typename E> class AdjacencyListGraph final { #include "private/ALGDefs.h" public: #include "private/ALGVertexIterator.h" #include "private/ALGEdgeIterator.h" using vertex_iterator = ALGVertexIterator<V, E>; using edge_iterator = ALGEdgeIterator<V, E>; explicit AdjacencyListGraph( std::size_t initialSize = 32 ) { graph.reserve( initialSize ); } ~AdjacencyListGraph() = default; AdjacencyListGraph( const AdjacencyList& ) = delete; AdjacencyListGraph& operator=( const AdjacencyList& ) = delete; template <typename ...Args> auto emplace( Args... args ) -> vertex_iterator; auto insert( V vertex ) -> vertex_iterator; template <typename ...Args> auto emplaceEdge( const V& source, const V& destination, Args... args ) -> edge_iterator; auto insertEdge( const V& source, const V& destination, E value ) -> edge_iterator; void erase( const V& vertex ); void erase( const vertex_iterator& iter ); void eraseByIndex( const std::size_t index ); using EdgeValue = std::tuple<V, E>; auto insertEdge( const V& source, EdgeValue&& tuple ) -> edge_iterator; void eraseEdge( const V& source, const V& destination, const E& edge ); auto cbegin() const -> vertex_iterator { return ALGVertexIterator<V,E>( graph.cbegin() ); } auto cend() const -> vertex_iterator { return ALGVertexIterator<V, E>( graph.cend() ); } auto cbegin( const V& vertex ) const -> edge_iterator; auto cend( const V& vertex ) const -> edge_iterator; auto operator[]( const std::size_t index ) const -> reference_type; bool exists( const V& vertex ) const; auto find( const V& vertex ) const -> vertex_iterator; auto find( const V& source, const V& destination ) const -> edge_iterator; std::size_t size() const { return graph.size(); } bool empty() const { return graph.empty(); } template <typename Visitor> void depthFirstSearch( const Visitor& visitor ) const; template <typename Visitor> void breadthFirstSearch( const Visitor& visitor ) const; private: auto indexOf( const V& vertex ) -> typename Graph::iterator; template <typename Visitor> void visitDfs( const std::size_t index, Visited& visited, const Visitor& visitor ) const; template <typename Visitor> void visitBfs( const std::size_t index, Visited& visited, const Visitor& visitor ) const; Graph graph; }; #include "private/ALGImpl.h" }
Definitions
// private/ALGDefs.h using value_type = V; using pointer_type = value_type const*; using reference_type = const value_type&; using edge_value_type = E; struct Edge { edge_value_type value; std::size_t index; bool operator==( const Edge& rhs ) const { return value == rhs.value && index == rhs.index; } }; using AdjacencyList = std::vector<Edge>; struct Vertex { Vertex( value_type value ) : value{ std::move( value ) } { edges.reserve( 8 ); } bool operator==( const Vertex& rhs ) const { return value == rhs.value; } value_type value; AdjacencyList edges; }; using Graph = std::vector<Vertex>; using Visited = std::vector<bool>;
Vertex Iterator
// private/ALGVertexIterator.h template <typename Vertex, typename Edge> struct ALGVertexIterator final { using difference_type = std::ptrdiff_t; using value_type = Vertex; using reference = Vertex&; using const_reference = const Vertex&; using pointer = Vertex*; using const_pointer = const Vertex*; using iterator_category = std::bidirectional_iterator_tag; ALGVertexIterator() = delete; ALGVertexIterator( const ALGVertexIterator& ) = default; ALGVertexIterator& operator= ( const ALGVertexIterator& ) = default; ALGVertexIterator& operator++() { ++iterator; return *this; } ALGVertexIterator& operator--() { --iterator; return *this; } bool operator==( const ALGVertexIterator& rhs ) const { return iterator == rhs.iterator; } bool operator!=( const ALGVertexIterator& rhs ) const { return !operator==( rhs ); } const_reference operator*() const { return iterator->value; } const_pointer operator->() const { return *( iterator->value ); } private: explicit ALGVertexIterator( typename Graph::const_iterator iterator ) : iterator{ iterator } {} typename Graph::const_iterator iterator; friend class AdjacencyListGraph<Vertex, Edge>; };
Edge Iterator
// private/ALGEdgeIterator.h template <typename VType, typename EType> struct ALGEdgeIterator final { using difference_type = std::ptrdiff_t; using value_type = EType; using reference = EType & ; using const_reference = const EType&; using pointer = EType * ; using const_pointer = const EType*; using vertex_index = std::size_t; using iterator_category = std::random_access_iterator_tag; ALGEdgeIterator() = delete; ALGEdgeIterator( const ALGEdgeIterator& ) = default; ALGEdgeIterator& operator= ( const ALGEdgeIterator& ) = default; ALGEdgeIterator& operator++() { ++iterator; return *this; } ALGEdgeIterator& operator--() { --iterator; return *this; } ALGEdgeIterator& operator+=( difference_type n ) { iterator.operator+=( n ); return *this; } ALGEdgeIterator operator+( difference_type n ) const { auto iter = iterator; iter.operator+=( n ); return { idx, iter }; } ALGEdgeIterator& operator-=( difference_type n ) { iterator.operator-=( n ); return *this; } const_reference operator[]( difference_type n ) const { operator+=( n ); return operator*(); } ALGEdgeIterator operator-( difference_type n ) const { auto iter = iterator; iter.operator-=( n ); return { idx, iter }; } bool operator==( const ALGEdgeIterator& rhs ) const { return idx == rhs.idx && iterator == rhs.iterator; } bool operator!=( const ALGEdgeIterator& rhs ) const { return !operator==( rhs ); } bool operator<( const ALGEdgeIterator& rhs ) const { if ( idx != rhs.idx ) throw std::runtime_error( "Iterators point to different vertices" ); return iterator.operator<( rhs.iterator ); } bool operator<=( const ALGEdgeIterator& rhs ) const { if ( idx != rhs.idx ) throw std::runtime_error( "Iterators point to different vertices" ); return iterator.operator<=( rhs.iterator ); } bool operator>( const ALGEdgeIterator& rhs ) const { if ( idx != rhs.idx ) throw std::runtime_error( "Iterators point to different vertices" ); return iterator.operator>( rhs.iterator ); } bool operator>=( const ALGEdgeIterator& rhs ) const { if ( idx != rhs.idx ) throw std::runtime_error( "Iterators point to different vertices" ); return iterator.operator>=( rhs.iterator ); } const_reference operator*() const { return iterator->value; } const_pointer operator->() const { return &( iterator->value ); } vertex_index index() const { return iterator->index; } private: explicit ALGEdgeIterator( difference_type index, typename AdjacencyList::const_iterator iterator ) : idx{ index }, iterator{ iterator } {} typename AdjacencyList::const_iterator iterator; difference_type idx; friend class AdjacencyListGraph<VType, EType>; };
Implementation
// private/ALGImpl.h template <typename V, typename E> template <typename ... Args> auto AdjacencyListGraph<V, E>::emplace( Args... args ) -> vertex_iterator { auto value{ args... }; return insert( std::move( value ) ); } template <typename V, typename E> auto AdjacencyListGraph<V, E>::insert( V value ) -> vertex_iterator { auto iter = std::find_if( graph.cbegin(), graph.cend(), [&value] ( const auto& vertex ) { return vertex.value == value; } ); if ( iter == graph.cend() ) { graph.emplace_back( Vertex( std::move( value ) ) ); iter = graph.cend(); --iter; } return ALGVertexIterator<V, E>( iter ); } template <typename V, typename E> template <typename ... Args> auto AdjacencyListGraph<V, E>::emplaceEdge( const V& source, const V& destination, Args... args ) -> edge_iterator { auto value{ args... }; return insertEdge( source, destination, std::move( value ) ); } template <typename V, typename E> auto AdjacencyListGraph<V, E>::insertEdge( const V& source, const V& destination, E value ) -> edge_iterator { auto siter = indexOf( source ); auto diter = indexOf( destination ); Edge edge{ std::move( value ), std::distance( graph.begin(), diter ) }; auto& vector = siter->edges; auto iter = std::find( vector.cbegin(), vector.cend(), edge ); if ( iter == vector.cend() ) { if ( vector.empty() ) vector.reserve( 4 ); vector.emplace_back( std::move( edge ) ); iter = vector.cend(); --iter; } return ALGEdgeIterator<V, E>{ std::distance( graph.begin(), siter ), iter }; } template <typename V, typename E> void AdjacencyListGraph<V, E>::erase( const V& vertex ) { erase( find( vertex ) ); } template <typename V, typename E> void AdjacencyListGraph<V, E>::erase( const vertex_iterator& iter ) { if ( iter.iterator != graph.cend() ) eraseByIndex( std::distance( graph.cbegin(), iter.iterator ) ); } template <typename V, typename E> void AdjacencyListGraph<V, E>::eraseByIndex( const std::size_t index ) { for ( auto& vertex: graph ) { vertex.edges.erase( std::remove_if( vertex.edges.begin(), vertex.edges.end(), [index] ( const auto& edge ) { return index == edge.index; } ), vertex.edges.end() ); } auto iter = graph.cbegin(); std::advance( iter, index ); graph.erase( iter ); } template <typename V, typename E> auto AdjacencyListGraph<V, E>::insertEdge( const V& source, EdgeValue&& tuple ) -> edge_iterator { return insertEdge( source, std::get<0>( tuple ), std::get<1>( tuple ) ); } template <typename V, typename E> void AdjacencyListGraph<V, E>::eraseEdge( const V& source, const V& destination, const E& edge ) { auto siter = indexOf( source ); if ( siter == graph.end() ) return; auto diter = indexOf( destination ); if ( diter == graph.end() ) return; const Edge value{ edge, std::distance( graph.begin(), diter ) }; auto& vector = siter->edges; auto liter = std::find( vector.cbegin(), vector.cend(), value ); if ( liter != vector.cend() ) vector.erase( liter ); } template <typename V, typename E> auto AdjacencyListGraph<V, E>::cbegin( const V& vertex ) const -> edge_iterator { auto iter = std::find( graph.begin(), graph.end(), vertex ); if ( iter == graph.end() ) throw std::out_of_range( "Vertex not in graph!" ); return ALGEdgeIterator<V, E>{ std::distance( graph.begin(), iter ), iter->edges.cbegin() }; } template <typename V, typename E> auto AdjacencyListGraph<V, E>::cend( const V& vertex ) const -> edge_iterator { auto iter = std::find( graph.begin(), graph.end(), vertex ); if ( iter == graph.end() ) throw std::out_of_range( "Vertex not in graph!" ); return ALGEdgeIterator<V, E>{ std::distance( graph.begin(), iter ), iter->edges.cend() }; } template <typename V, typename E> auto AdjacencyListGraph<V, E>::operator[]( const std::size_t index ) const -> reference_type { if ( index >= graph.size() ) throw std::out_of_range( "Index out of bounds!" ); return graph[index].value; } template <typename V, typename E> bool AdjacencyListGraph<V, E>::exists( const V& vertex ) const { const auto iter = std::find_if( graph.cbegin(), graph.cend(), [&vertex]( const auto& v ) { return v.value == vertex; } ); return iter != graph.cend(); } template <typename V, typename E> auto AdjacencyListGraph<V, E>::find( const V& vertex ) const -> vertex_iterator { const auto iter = std::find_if( graph.cbegin(), graph.cend(), [&vertex]( const auto& v ) { return v.value == vertex; } ); return ALGVertexIterator<V, E>{ iter }; } template <typename V, typename E> auto AdjacencyListGraph<V, E>::find( const V& source, const V& destination ) const -> edge_iterator { const auto siter = std::find_if( graph.cbegin(), graph.cend(), [&source]( const auto& v ) { return v.value == source; } ); if ( siter == graph.cend() ) return cend( source ); const auto diter = std::find_if( graph.cbegin(), graph.cend(), [&destination]( const auto& v ) { return v.value == destination; } ); if ( diter == graph.cend() ) return cend( source ); const auto index = std::distance( graph.cbegin(), diter ); const auto iter = std::find_if( siter->edges.cbegin(), siter->edges.cend(), [index] ( const auto& e ) { return e.index == index; } ); return ALGEdgeIterator<V, E>{ std::distance( graph.cbegin(), siter ), iter }; } template <typename V, typename E> template <typename Visitor> void AdjacencyListGraph<V, E>::depthFirstSearch( const Visitor& visitor ) const { if ( empty() ) return; Visited visited( size(), false ); for ( std::size_t i = 0; i < graph.size(); ++i ) { if ( !visited[i] ) visitDfs( i, visited, visitor ); } } template <typename V, typename E> template <typename Visitor> void AdjacencyListGraph<V, E>::breadthFirstSearch( const Visitor& visitor ) const { if ( empty() ) return; Visited visited( size(), false ); for ( std::size_t i = 0; i < graph.size(); ++i ) { if ( !visited[i] ) visitBfs( i, visited, visitor ); } } template <typename V, typename E> auto AdjacencyListGraph<V, E>::indexOf( const V& vertex ) -> typename Graph::iterator { auto iter = std::find( graph.begin(), graph.end(), vertex ); if ( iter == graph.end() ) { insert( vertex ); iter = graph.end(); --iter; } return iter; } template <typename V, typename E> template <typename Visitor> void AdjacencyListGraph<V, E>::visitDfs( const std::size_t index, Visited& visited, const Visitor& visitor ) const { std::stack<std::size_t> stack; stack.push( index ); while ( !stack.empty() ) { std::size_t i = stack.top(); stack.pop(); const auto& node = graph[i]; if ( !visited[i] ) { visited[i] = true; visitor( node.value ); } for ( auto iter = node.edges.cbegin(); iter != node.edges.cend(); ++iter ) { if ( !visited[iter->index] ) stack.push( iter->index ); } } } template<typename V, typename E> template<typename Visitor> void AdjacencyListGraph<V, E>::visitBfs( const std::size_t index, Visited & visited, const Visitor & visitor ) const { std::queue<std::size_t> queue; queue.push( 0 ); while ( !queue.empty() ) { auto i = queue.front(); queue.pop(); const auto& node = graph[i]; visitor( node.value ); if ( !visited[i] ) visited[i] = true; for ( auto iter = node.edges.cbegin(); iter != node.edges.cend(); ++iter ) { if ( !visited[iter->index] ) { visited[iter->index] = true; queue.push( iter->index ); } } } }
Test
// AdjacencyListGraph.cpp #include "catch.hpp" #include "../main/AdjacencyListGraph.h" #include <iostream> #include <sstream> namespace csc280::graph { struct NumberVisitor { void operator()( const uint32_t value ) const { ss << value << ", "; } std::string operator()() const { return ss.str(); } private: mutable std::stringstream ss; }; } SCENARIO( "Creating an adjacency list based graph of strings" ) { GIVEN( "A graph of strings with a counter" ) { csc280::AdjacencyListGraph<std::string, std::size_t> graph; WHEN( "Vertices are added to the graph" ) { REQUIRE( graph.empty() ); auto iter = graph.emplace( "Chicago" ); REQUIRE( iter != graph.cend() ); REQUIRE( 1 == graph.size() ); REQUIRE_FALSE( graph.empty() ); REQUIRE( "Chicago" == graph[0] ); REQUIRE( "Chicago" == *iter ); REQUIRE( "Chicago" == *graph.find( "Chicago") ); REQUIRE( graph.exists( "Chicago" ) ); REQUIRE_FALSE( graph.exists( "New York" ) ); iter = graph.emplace( "Chicago" ); REQUIRE( iter != graph.cend() ); REQUIRE( 1 == graph.size() ); iter = graph.emplace( "New York" ); REQUIRE( iter != graph.cend() ); REQUIRE( 2 == graph.size() ); REQUIRE( "New York" == graph[1] ); REQUIRE( "New York" == *iter ); REQUIRE( "New York" == *graph.find( "New York") ); REQUIRE( graph.exists( "New York" ) ); THEN( "Edges may be added to the graph" ) { auto eiter = graph.emplaceEdge( "Chicago", "New York", 900 ); REQUIRE( eiter != graph.cend( "Chicago" ) ); REQUIRE( 2 == graph.size() ); REQUIRE( 900 == *eiter ); REQUIRE( 1 == eiter.index() ); eiter = graph.emplaceEdge( "Chicago", "New York", 1000 ); REQUIRE( eiter != graph.cend( "Chicago" ) ); REQUIRE( 2 == graph.size() ); eiter = graph.emplaceEdge( "New York", "Chicago", 950 ); REQUIRE( eiter != graph.cend( "New York" ) ); REQUIRE( 2 == graph.size() ); graph.eraseEdge( "New York", "Chicago", 950 ); REQUIRE( graph.cbegin( "New York" ) == graph.cend( "New York" ) ); eiter = graph.insertEdge( "New York", std::make_tuple( "Chicago", 950 ) ); REQUIRE( eiter != graph.cend( "New York" ) ); REQUIRE( 2 == graph.size() ); } AND_THEN( "New vertex and edge may be added to graph in single operation" ) { graph.insertEdge( "Springfield", "Chicago", 60 ); REQUIRE( 3 == graph.size() ); graph.insertEdge( "Springfield", "New York", 1000 ); REQUIRE( 3 == graph.size() ); REQUIRE( graph.exists( "Springfield" ) ); REQUIRE( "Springfield" == *graph.find( "Springfield") ); } AND_THEN( "Erasing vertex removes all dependent edges" ) { graph.erase( "Chicago" ); REQUIRE_FALSE( graph.exists( "Chicago" ) ); const auto eiter = graph.find( "New York", "Chicago" ); REQUIRE( eiter == graph.cend( "New York" ) ); } } } } SCENARIO( "Depth first search traversal for adjacency list graph" ) { GIVEN( "A graph of numbers" ) { csc280::AdjacencyListGraph<uint32_t, bool> graph; graph.insertEdge( 1, 0, false ); graph.insertEdge( 2, 1, false ); graph.insertEdge( 3, 4, false ); graph.insertEdge( 4, 0, false ); WHEN( "Traversing in depth first search mode" ) { const auto visitor = csc280::graph::NumberVisitor{}; graph.depthFirstSearch( visitor ); REQUIRE( "1, 0, 2, 3, 4, " == visitor() ); } } } SCENARIO( "Breadth first search traversal for adjacency list graph" ) { GIVEN( "A graph of numbers" ) { csc280::AdjacencyListGraph<uint32_t, bool> graph; graph.insertEdge( 0, 1, false ); graph.insertEdge( 0, 2, false ); graph.insertEdge( 1, 2, false ); graph.insertEdge( 2, 0, false ); graph.insertEdge( 2, 3, false ); graph.insertEdge( 3, 3, false ); WHEN( "Traversing in depth first search mode" ) { const auto visitor = csc280::graph::NumberVisitor{}; graph.breadthFirstSearch( visitor ); REQUIRE( "0, 1, 2, 3, " == visitor() ); } } }