-
Notifications
You must be signed in to change notification settings - Fork 1
Graph traversal #11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Graph traversal #11
Changes from all commits
c6dea0a
c6255c3
27b7e0a
c012231
a0ccc29
67a1b26
fa2602a
397dd51
15c45ad
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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") | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Raising a different try:
# something that raises a KeyError
except KeyError as e:
e.message = "Node {} does not exist".format(start)
raise |
||
|
|
||
| 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() | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Recursive methods can be quite expensive in terms of resources, especially with large graphs. Can you think of a way to accomplish this without using a recursive approach? |
||
|
|
||
| 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) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. since |
||
| queue.enqueue(child) | ||
|
|
||
| return explored.keys() | ||
| except KeyError: | ||
| raise KeyError("Node does not exist") | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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] | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It'd be nice to see traversal tests for graphs with un-connected nodes, just to ensure that you don't have a way to mistakenly teleport between unconnected nodes. |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Leave a blank line between the first and second lines of a good docstring.