Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down
Empty file added __init__.py
Empty file.
Empty file added graph/__init__.py
Empty file.
57 changes: 50 additions & 7 deletions graph/simple_graph.py
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"""

Expand All @@ -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
Expand Down Expand Up @@ -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.
Copy link

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.

"""
try:
explored = OrderedDict()
return self._depth_first_traversal(start, explored)
except KeyError:
raise KeyError("Node does not exist")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Raising a different KeyError object causes the traceback to change. This can separate the source of the error from the reporting location which will cause confusion in your users. Try this instead:

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()
Copy link

Choose a reason for hiding this comment

The 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)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since setdefault would not change the ordering of a key that has already been inserted, it'd be safe to do this before you bother checking for the presence of the child in the explored dict. If you are doing it here, there's no reason not to simply set the value at the child key.

queue.enqueue(child)

return explored.keys()
except KeyError:
raise KeyError("Node does not exist")
123 changes: 123 additions & 0 deletions graph/test_simple_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 == {}
Expand All @@ -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)
Expand Down Expand Up @@ -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]
Copy link

Choose a reason for hiding this comment

The 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.