diff --git a/README.md b/README.md index 2793040..2332352 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,9 @@ This repository holds sample code for a number of classic data structures implem - This implementation of the priority queue imports our binary heap module and passes tuples consisting of priority, seniority, and value into a heap. This implementation is adapted from the Python Cookbook example cited in the Resources section. * Graph (unweighted, directed) - - As described from [Python Patterns - Implementing Graphs] (https://www.python.org/doc/essays/graphs/) "Graphs are networks consisting of nodes connected by edges or arcs. In directed graphs, the connections between nodes have a direction, and are called arcs." + - As described from [Python Patterns - Implementing Graphs](https://www.python.org/doc/essays/graphs/) "Graphs are networks consisting of nodes connected by edges or arcs. In directed graphs, the connections between nodes have a direction, and are called arcs." + - Graph Traversal + - Graph traversal is explored in a [depth first](http://en.wikipedia.org/wiki/Depth-first_search) and [breadth first](http://en.wikipedia.org/wiki/Breadth-first_search) style. ## Resources [Linked Lists Wiki](http://en.wikipedia.org/wiki/Linked_list) @@ -29,7 +31,7 @@ This repository holds sample code for a number of classic data structures implem [Binary Heap Visualization](http://www.comp.nus.edu.sg/~stevenha/visualization/heap.html) [Priority Queue Wiki](http://en.wikipedia.org/wiki/Priority_queue) [Implementing a Priority Queue from a Binary Heap](https://www.safaribooksonline.com/library/view/python-cookbook-3rd/9781449357337/ch01s05.html) -[Python Patterns - Implementing Graphs] (https://www.python.org/doc/essays/graphs/) +[Python Patterns - Implementing Graphs](https://www.python.org/doc/essays/graphs/) Pytest - for testing structure functions diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/graph/__init__.py b/graph/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/graph/simple_graph.py b/graph/simple_graph.py index bdba8ea..2cc2f47 100644 --- a/graph/simple_graph.py +++ b/graph/simple_graph.py @@ -1,3 +1,7 @@ +from collections import OrderedDict +from queue import Queue + + class Graph(object): """Implements a graph data structure""" @@ -10,17 +14,13 @@ def nodes(self): def edges(self): """return a list of all edges in the graph""" - edge_list = [] - for key, value in self.graph_dict.items(): - for item in value: - edge_list.append((key, item)) - return edge_list - # return [(key, value) for key, value in self.graph_dict.items()] + return [(key, node) for key, value in + self.graph_dict.iteritems() for node in value] def add_node(self, node): """add a new node 'n' to the graph""" - self.graph_dict[node] = [] + self.graph_dict.setdefault(node, []) def add_edge(self, node_1, node_2): """add a new edge to the graph connecting 'n1' and 'n2', if either n1 @@ -73,3 +73,46 @@ def adjacent(self, node_1, node_2): return node_2 in self.graph_dict[node_1] except KeyError: raise KeyError("First node not found") + + def depth_first_traversal(self, start): + """Perform a full depth-first traversal of the graph beginning at start. + Return the full visited path when traversal is complete. + """ + try: + explored = OrderedDict() + return self._depth_first_traversal(start, explored) + except KeyError: + raise KeyError("Node does not exist") + + def _depth_first_traversal(self, start, explored): + """Helper function for depth_first_traversal for recursion""" + explored.setdefault(start, 1) + + for child in self.graph_dict[start]: + if child not in explored: + self._depth_first_traversal(child, explored) + + return explored.keys() + + def breadth_first_traversal(self, start): + """Perform a full breadth-first traversal of the graph, beginning at + start. Return the full visited path when traversal is complete. + """ + try: + explored = OrderedDict() + queue = Queue() + explored.setdefault(start, 1) + + queue.enqueue(start) + + while queue.size(): + node = queue.dequeue() + + for child in self.graph_dict[node]: + if child not in explored: + explored.setdefault(child, 1) + queue.enqueue(child) + + return explored.keys() + except KeyError: + raise KeyError("Node does not exist") diff --git a/graph/test_simple_graph.py b/graph/test_simple_graph.py index e611354..2c7e24a 100644 --- a/graph/test_simple_graph.py +++ b/graph/test_simple_graph.py @@ -15,6 +15,41 @@ def test_graph(): return test_graph +@pytest.fixture(scope="function") +def test_depth_traversal_graph(): + test_graph = Graph() + for i in range(10): + test_graph.add_node(i) + test_graph.add_edge(0, 1) + test_graph.add_edge(0, 2) + test_graph.add_edge(0, 3) + test_graph.add_edge(1, 4) + test_graph.add_edge(1, 5) + test_graph.add_edge(1, 8) + test_graph.add_edge(5, 6) + test_graph.add_edge(6, 7) + test_graph.add_edge(2, 9) + + return test_graph + + +@pytest.fixture(scope="function") +def test_breadth_traversal_graph(): + test_graph = Graph() + for i in range(1, 10): + test_graph.add_node(i) + test_graph.add_edge(1, 2) + test_graph.add_edge(1, 3) + test_graph.add_edge(1, 4) + test_graph.add_edge(2, 5) + test_graph.add_edge(2, 6) + test_graph.add_edge(4, 7) + test_graph.add_edge(4, 8) + test_graph.add_edge(5, 9) + + return test_graph + + def test_constructor(): test = Graph() assert test.graph_dict == {} @@ -26,6 +61,11 @@ def test_add_node(): assert 5 in test.graph_dict +def test_add_node_if_already_present(test_graph): + test_graph.add_node(5) + assert 42 in test_graph.graph_dict[5] + + def test_add_edge(): test = Graph() test.add_node(5) @@ -123,3 +163,86 @@ def test_adjacent_second_node_not_found(test_graph): with pytest.raises(KeyError) as e: test_graph.adjacent("test", 47) assert 'Second node not found' in str(e.value) + + +def test_depth_first_from_node_0(test_depth_traversal_graph): + assert test_depth_traversal_graph.depth_first_traversal(0) == [ + 0, 1, 4, 5, 6, 7, 8, 2, 9, 3] + + +def test_depth_first_from_node_1(test_depth_traversal_graph): + assert test_depth_traversal_graph.depth_first_traversal(1) == [ + 1, 4, 5, 6, 7, 8] + + +def test_depth_first_no_edges(): + test = Graph() + test.add_node(55) + test.add_node("test") + test.add_node(2) + assert test.depth_first_traversal(55) == [55] + + +def test_depth_multiple_edges(): + test = Graph() + for i in range(5): + test.add_node(i) + test.add_edge(0, 1) + test.add_edge(0, 2) + test.add_edge(1, 2) + test.add_edge(1, 3) + test.add_edge(2, 3) + test.add_edge(3, 4) + + assert test.depth_first_traversal(0) == [0, 1, 2, 3, 4] + + +def test_depth_first_from_non_existent_node(test_depth_traversal_graph): + with pytest.raises(KeyError) as e: + test_depth_traversal_graph.depth_first_traversal(11) + assert "Node does not exist" in str(e.value) + + +def test_depth_cyclic(test_graph): + assert test_graph.depth_first_traversal(5) == [5, 42, "test"] + + +def test_breadth_first(test_breadth_traversal_graph): + assert ( + test_breadth_traversal_graph.breadth_first_traversal(1) == range(1, 10)) + + +def test_breadth_first_from_node_3(test_breadth_traversal_graph): + assert test_breadth_traversal_graph.breadth_first_traversal(3) == [3] + + +def test_breadth_first_from_non_existent_node(test_breadth_traversal_graph): + with pytest.raises(KeyError) as e: + test_breadth_traversal_graph.breadth_first_traversal(11) + assert "Node does not exist" in str(e.value) + + +def test_breadth_cyclic(test_graph): + assert test_graph.breadth_first_traversal(5) == [5, 42, "test"] + + +def test_breadth_first_no_edges(): + test = Graph() + test.add_node(55) + test.add_node("test") + test.add_node(2) + assert test.breadth_first_traversal(55) == [55] + + +def test_breadth_multiple_edges(): + test = Graph() + for i in range(5): + test.add_node(i) + test.add_edge(0, 1) + test.add_edge(0, 2) + test.add_edge(1, 2) + test.add_edge(1, 3) + test.add_edge(2, 3) + test.add_edge(3, 4) + + assert test.breadth_first_traversal(0) == [0, 1, 2, 3, 4]