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..98104f2 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. @@ -18,8 +20,17 @@ This repository holds sample code for a number of classic data structures implem * Priority Queue with Heap - 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) +* Graph (weighted, 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 + - Shortest Path Algorithms + - Dijkstra's Algorithm + - Bellman-Ford Algorithm + - Both algorithms use iterative relaxation to find the shortest path between a starting node and target node. Through its use of a priority queue, Dijkstra's algorithm picks the minimum weighted node from among those it has not yet explored. By comparison, the Bellman-Ford algorithm processes all of the edges n-1 times, where n is the total number of nodes in the graph. This thoroughness increases the runtime of Bellman-Ford but allows it to handle negative weights, which Dijkstra's algorithm cannot. + ## Resources [Linked Lists Wiki](http://en.wikipedia.org/wiki/Linked_list) @@ -29,10 +40,14 @@ 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/) +[Dijkstra's Algorithm](http://en.wikipedia.org/wiki/Dijkstra%27s_algorithm) +[Bellman-Ford Algorithm](http://en.wikipedia.org/wiki/Bellman%E2%80%93Ford_algorithm) 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..f5fe53b 100644 --- a/graph/simple_graph.py +++ b/graph/simple_graph.py @@ -1,3 +1,11 @@ +from collections import OrderedDict +from queue import Queue +from Queue import PriorityQueue + + +W_DEFAULT = 1 + + class Graph(object): """Implements a graph data structure""" @@ -11,43 +19,40 @@ 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.items()] 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,118 @@ 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") + + def dijkstra_shortest(self, start, end): + """Implementation of Dijkstra's shortest path algorithm, returns a list + of shortest path from start to end""" + if start is end: + return [start] + distance = {start: 0} + previous_node = {} + pqueue = PriorityQueue() + for node in self.nodes(): + if node is not start: + distance[node] = float('inf') + previous_node['node'] = None + pqueue.put((distance[node], node)) # (priority, data) + + while pqueue: + current = pqueue.get()[1] + if current == end: + break + for neighbor in self.neighbors(current): + alt = distance[current] + self.graph_dict[current][neighbor] + if alt < distance[neighbor]: + distance[neighbor] = alt + previous_node[neighbor] = current + pqueue.put((distance[neighbor], neighbor)) + + path = [] + while end is not start: + path.append(end) + try: + end = previous_node[end] + except KeyError: + return "No Path Found" + path.append(start) + path.reverse() + return path + + def bellman_ford_shortest(self, start, end): + """Implementation of Bellman-Ford's shortest path algorithm, returns a + list of shortest path from start to end""" + if start is end: + return [start] + distance = {start: 0} + previous_node = {} + # Step 1: initialize graph + for node in self.nodes(): + if node is not start: + distance[node] = float('inf') + previous_node[node] = None + + # Step 2: relax edges repeatedly + for i in range(1, len(self.nodes()) - 1): + for edge in self.edges(): + if distance[edge[0]] + edge[2] < distance[edge[1]]: + distance[edge[1]] = distance[edge[0]] + edge[2] + previous_node[edge[1]] = edge[0] + + # Step 3: check for negative-weight cycles + for edge in self.edges(): + if distance[edge[0]] + edge[2] < distance[edge[1]]: + raise Exception("Graph contains a negative-weight cycle") + + path = [] + while end is not start: + path.append(end) + try: + end = previous_node[end] + except KeyError: + return "No Path Found" + path.append(start) + path.reverse() + return path diff --git a/graph/test_simple_graph.py b/graph/test_simple_graph.py index 01bbb10..f4836af 100644 --- a/graph/test_simple_graph.py +++ b/graph/test_simple_graph.py @@ -1,5 +1,6 @@ import pytest from simple_graph import Graph +from collections import OrderedDict @pytest.fixture(scope="function") @@ -9,12 +10,97 @@ 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 + + +@pytest.fixture(scope="function") +def test_weighted_graph(): + test_graph = Graph() + test_graph.add_edge("A", "B", 2) + test_graph.add_edge("A", "C", 5) + test_graph.add_edge("B", "C", 2) + test_graph.add_edge("B", "D", 6) + test_graph.add_edge("C", "D", 2) + + return test_graph + +@pytest.fixture(scope="function") +def test_complex_weighted_graph(): + test_graph = Graph() + test_graph.add_edge("A", "B", 1) + test_graph.add_edge("A", "D", 7) + test_graph.add_edge("B", "G", 5) + test_graph.add_edge("B", "D", 4) + test_graph.add_edge("C", "B", 4) + test_graph.add_edge("C", "E", 6) + test_graph.add_edge("C", "F", 4) + test_graph.add_edge("D", "B", 7) + test_graph.add_edge("D", "F", 5) + test_graph.add_edge("D", "H", 1) + test_graph.add_edge("E", "G", 3) + test_graph.add_edge("F", "B", 2) + test_graph.add_edge("F", "G", 9) + test_graph.add_edge("F", "D", 7) + test_graph.add_edge("G", "C", 9) + test_graph.add_edge("G", "F", 7) + + return test_graph + + +@pytest.fixture(scope="function") +def test_loop_weighted_graph(): + test_graph = Graph() + test_graph.add_edge("A", "B", 1) + test_graph.add_edge("B", "C", 2) + test_graph.add_edge("B", "E", 7) + test_graph.add_edge("C", "B", 4) + test_graph.add_edge("C", "D", 6) + test_graph.add_edge("D", "C", 1) + test_graph.add_edge("D", "E", 7) + test_graph.add_edge("E", "D", 3) + test_graph.add_edge("E", "B", 10) + + return test_graph + + def test_constructor(): test = Graph() assert test.graph_dict == {} @@ -56,12 +142,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 +183,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 +191,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 +226,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_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_dijkstra(test_weighted_graph): + assert test_weighted_graph.dijkstra_shortest('A', 'D') == ['A', 'B', + 'C', 'D'] + + +def test_dijkstra_complex(test_complex_weighted_graph): + assert test_complex_weighted_graph.dijkstra_shortest('G', 'D') == ['G', 'F', + 'B', 'D'] + + +def test_dijkstra_no_path(test_complex_weighted_graph): + assert test_complex_weighted_graph.dijkstra_shortest('F', 'A') == ( + "No Path Found") + + +def test_dijkstra_same_start_end(test_complex_weighted_graph): + assert test_complex_weighted_graph.dijkstra_shortest('A', 'A') == ['A'] + + +def test_dijkstra_loop(test_loop_weighted_graph): + assert test_loop_weighted_graph.dijkstra_shortest('D', 'A') == ( + "No Path Found") + + +def test_bellman_ford(test_weighted_graph): + assert test_weighted_graph.bellman_ford_shortest('A', 'D') == ['A', 'B', + 'C', 'D'] + + +def test_belman_ford_complex(test_complex_weighted_graph): + assert test_complex_weighted_graph.bellman_ford_shortest('G', 'D') == [ + 'G', 'F', 'B', 'D'] + + +def test_bellman_ford_no_path(test_complex_weighted_graph): + assert test_complex_weighted_graph.bellman_ford_shortest('F', 'A') == ( + "No Path Found") + + +def test_bellman_ford_same_start_end(test_complex_weighted_graph): + assert test_complex_weighted_graph.bellman_ford_shortest('A', 'A') == ['A']