diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..38e8f2d --- /dev/null +++ b/.travis.yml @@ -0,0 +1,4 @@ +language: python +python: + - "2.7" +script: py.test \ No newline at end of file diff --git a/README.md b/README.md index ccf9f80..8f49e76 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +[![Build Status](https://travis-ci.org/henrykh/data-structures.svg)](https://travis-ci.org/henrykh/data-structures) + # data-structures This repository holds sample code for a number of classic data structures implemented in Python. @@ -20,6 +22,10 @@ This repository holds sample code for a number of classic data structures implem * 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." + - 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. + - Graph weighting + - Edges accept value weights or default to one if none is provided ## Resources [Linked Lists Wiki](http://en.wikipedia.org/wiki/Linked_list) @@ -33,6 +39,8 @@ This repository holds sample code for a number of classic data structures implem Pytest - for testing structure functions +Travis CI + ##Collaborators Joel Stanner 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 7aee42c..1dd5df9 100644 --- a/graph/simple_graph.py +++ b/graph/simple_graph.py @@ -1,3 +1,10 @@ +from collections import OrderedDict +from queue import Queue + + +W_DEFAULT = 1 + + class Graph(object): """Implements a graph data structure""" @@ -11,43 +18,41 @@ def nodes(self): def edges(self): """return a list of all edges in the graph""" - return [(key, node) for key, value in - self.graph_dict.iteritems() for node in value] + return [(key, node, weight) for key, edge_dict in + self.graph_dict.iteritems() + for node, weight in edge_dict.iteritems()] def add_node(self, node): """add a new node 'n' to the graph""" - self.graph_dict.setdefault(node,[]) + self.graph_dict.setdefault(node, OrderedDict()) - def add_edge(self, node_1, node_2): + def add_edge(self, node_1, node_2, weight=W_DEFAULT): """add a new edge to the graph connecting 'n1' and 'n2', if either n1 or n2 are not already present in the graph, they are added. """ try: - self.graph_dict[node_1].append(node_2) + self.add_node(node_2) + self.graph_dict[node_1][node_2] = weight except KeyError: self.add_node(node_1) - self.graph_dict[node_1].append(node_2) - if node_2 not in self.nodes(): - self.add_node(node_2) + self.graph_dict[node_1][node_2] = weight def del_node(self, node): """delete the node 'n' from the graph""" try: del self.graph_dict[node] - for val_list in self.graph_dict.values(): - if node in val_list: - val_list.remove(node) + for val_dict in self.graph_dict.values(): + if node in val_dict: + del val_dict[node] except KeyError: raise KeyError("Node not found") def del_edge(self, node_1, node_2): """delete the edge connecting 'n1' and 'n2' from the graph""" try: - self.graph_dict[node_1].remove(node_2) + del self.graph_dict[node_1][node_2] except KeyError: - raise KeyError("First node not found") - except ValueError: - raise ValueError("Edge not found") + raise KeyError("Edge not found") def has_node(self, node): """True if node 'n' is contained in the graph, False if not""" @@ -56,7 +61,7 @@ def has_node(self, node): def neighbors(self, node): """return the list of all nodes connected to 'n' by edges""" try: - return self.graph_dict[node] + return self.graph_dict[node].keys() except KeyError: raise KeyError("Node not found") @@ -69,3 +74,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 01bbb10..2bb61f0 100644 --- a/graph/test_simple_graph.py +++ b/graph/test_simple_graph.py @@ -1,6 +1,6 @@ import pytest from simple_graph import Graph - +from collections import OrderedDict @pytest.fixture(scope="function") def test_graph(): @@ -9,12 +9,47 @@ def test_graph(): test_graph.add_node(42) test_graph.add_node("test") test_graph.add_edge(5, 42) - test_graph.add_edge(42, "test") + test_graph.add_edge(42, "test", 1000) test_graph.add_edge("test", 5) 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 == {} @@ -56,12 +91,24 @@ def test_add_edge_second_node_new(): assert "test" in test.graph_dict[55] +def test_add_edge_with_default_weight(): + test = Graph() + test.add_edge(1, 2) + assert test.graph_dict[1] == OrderedDict([(2, 1)]) + + +def test_add_edge_with_weight_parameter(): + test = Graph() + test.add_edge(1, 2, 10000) + assert test.graph_dict[1] == OrderedDict([(2, 10000)]) + + def test_nodes(test_graph): assert test_graph.nodes() == ["test", 42, 5] def test_edges(test_graph): - assert test_graph.edges() == [("test", 5), (42, "test"), (5, 42)] + assert test_graph.edges() == [("test", 5, 1), (42, "test", 1000), (5, 42, 1)] def test_del_node(test_graph): @@ -85,7 +132,7 @@ def test_del_edge(test_graph): def test_del_edge_not_found(test_graph): - with pytest.raises(ValueError) as e: + with pytest.raises(KeyError) as e: test_graph.del_edge(5, "test") assert "Edge not found" in str(e.value) @@ -93,7 +140,7 @@ def test_del_edge_not_found(test_graph): def test_del_edge_node_not_found(test_graph): with pytest.raises(KeyError) as e: test_graph.del_edge(8, "test") - assert "First node not found" in str(e.value) + assert "Edge not found" in str(e.value) def test_has_node_true(test_graph): @@ -128,3 +175,43 @@ 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_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)