Skip to content

Commit 1dbcffb

Browse files
committed
Graph traversal, DFS and BFS
1 parent 48ce03b commit 1dbcffb

File tree

3 files changed

+264
-0
lines changed

3 files changed

+264
-0
lines changed

6-graphs/GraphTraversalNotes.md

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
### Graph Traversal
2+
Travelling along a graph is kind of like travelling along a tree, except that there is no root node and paths can loop back to each other. So most of these methods make use of flagging a node as previously seen.
3+
4+
Searching a graph and travelling a graph are also quite similar--with searching a graph you want to stop when you find the item you were looking for and with travelling, you'd keep going through all of them til there were no more to go through.
5+
6+
### Depth First Search
7+
In a DFS, you travel all the way down one path til there's no more and then will retrace. Iterative implementation of this may use a stack, where nodes are loaded onto the stack and marked as seen, and when you hit the end of one path you'd pop a node off the stack to go back to it and traverse any edges on that one.
8+
9+
Can also be written recursively where the base case is one where there are no more nodes to explore on a path, and you go back up a level of recursion to keep looking.
10+
11+
### Breadth First Search
12+
In a BFS, you travel all the edges connected to the one that you're on before you travel the edges of each one of those next nodes. It's like going by levels, which makes it kind of like turning a graph into a tree. You can use a queue for this, where you mark nodes as seen and when you finish all the edges of the node you are at (queuing all the way), you then peel the next one out of the queue and traverse the levels for that node.
13+

6-graphs/GraphTraversalQuiz-mine.py

+251
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
class Node(object):
2+
def __init__(self, value):
3+
self.value = value
4+
self.edges = []
5+
self.visited = False
6+
7+
class Edge(object):
8+
def __init__(self, value, node_from, node_to):
9+
self.value = value
10+
self.node_from = node_from
11+
self.node_to = node_to
12+
13+
# You only need to change code with docs strings that have TODO.
14+
# Specifically: Graph.dfs_helper and Graph.bfs
15+
# New methods have been added to associate node numbers with names
16+
# Specifically: Graph.set_node_names
17+
# and the methods ending in "_names" which will print names instead
18+
# of node numbers
19+
20+
class Graph(object):
21+
def __init__(self, nodes=None, edges=None):
22+
self.nodes = nodes or []
23+
self.edges = edges or []
24+
self.node_names = []
25+
self._node_map = {}
26+
27+
def set_node_names(self, names):
28+
"""The Nth name in names should correspond to node number N.
29+
Node numbers are 0 based (starting at 0).
30+
"""
31+
self.node_names = list(names)
32+
33+
def insert_node(self, new_node_val):
34+
"Insert a new node with value new_node_val"
35+
new_node = Node(new_node_val)
36+
self.nodes.append(new_node)
37+
self._node_map[new_node_val] = new_node
38+
return new_node
39+
40+
def insert_edge(self, new_edge_val, node_from_val, node_to_val):
41+
"Insert a new edge, creating new nodes if necessary"
42+
nodes = {node_from_val: None, node_to_val: None}
43+
for node in self.nodes:
44+
if node.value in nodes:
45+
nodes[node.value] = node
46+
if all(nodes.values()):
47+
break
48+
for node_val in nodes:
49+
nodes[node_val] = nodes[node_val] or self.insert_node(node_val)
50+
node_from = nodes[node_from_val]
51+
node_to = nodes[node_to_val]
52+
new_edge = Edge(new_edge_val, node_from, node_to)
53+
node_from.edges.append(new_edge)
54+
node_to.edges.append(new_edge)
55+
self.edges.append(new_edge)
56+
57+
def get_edge_list(self):
58+
"""Return a list of triples that looks like this:
59+
(Edge Value, From Node, To Node)"""
60+
return [(e.value, e.node_from.value, e.node_to.value)
61+
for e in self.edges]
62+
63+
def get_edge_list_names(self):
64+
"""Return a list of triples that looks like this:
65+
(Edge Value, From Node Name, To Node Name)"""
66+
return [(edge.value,
67+
self.node_names[edge.node_from.value],
68+
self.node_names[edge.node_to.value])
69+
for edge in self.edges]
70+
71+
def get_adjacency_list(self):
72+
"""Return a list of lists.
73+
The indecies of the outer list represent "from" nodes.
74+
Each section in the list will store a list
75+
of tuples that looks like this:
76+
(To Node, Edge Value)"""
77+
max_index = self.find_max_index()
78+
adjacency_list = [[] for _ in range(max_index)]
79+
for edg in self.edges:
80+
from_value, to_value = edg.node_from.value, edg.node_to.value
81+
adjacency_list[from_value].append((to_value, edg.value))
82+
return [a or None for a in adjacency_list] # replace []'s with None
83+
84+
def get_adjacency_list_names(self):
85+
"""Each section in the list will store a list
86+
of tuples that looks like this:
87+
(To Node Name, Edge Value).
88+
Node names should come from the names set
89+
with set_node_names."""
90+
adjacency_list = self.get_adjacency_list()
91+
def convert_to_names(pair, graph=self):
92+
node_number, value = pair
93+
return (graph.node_names[node_number], value)
94+
def map_conversion(adjacency_list_for_node):
95+
if adjacency_list_for_node is None:
96+
return None
97+
return map(convert_to_names, adjacency_list_for_node)
98+
return [map_conversion(adjacency_list_for_node)
99+
for adjacency_list_for_node in adjacency_list]
100+
101+
def get_adjacency_matrix(self):
102+
"""Return a matrix, or 2D list.
103+
Row numbers represent from nodes,
104+
column numbers represent to nodes.
105+
Store the edge values in each spot,
106+
and a 0 if no edge exists."""
107+
max_index = self.find_max_index()
108+
adjacency_matrix = [[0] * (max_index) for _ in range(max_index)]
109+
for edg in self.edges:
110+
from_index, to_index = edg.node_from.value, edg.node_to.value
111+
adjacency_matrix[from_index][to_index] = edg.value
112+
return adjacency_matrix
113+
114+
def find_max_index(self):
115+
"""Return the highest found node number
116+
Or the length of the node names if set with set_node_names()."""
117+
if len(self.node_names) > 0:
118+
return len(self.node_names)
119+
max_index = -1
120+
if len(self.nodes):
121+
for node in self.nodes:
122+
if node.value > max_index:
123+
max_index = node.value
124+
return max_index
125+
126+
def find_node(self, node_number):
127+
"Return the node with value node_number or None"
128+
return self._node_map.get(node_number)
129+
130+
def _clear_visited(self):
131+
for node in self.nodes:
132+
node.visited = False
133+
134+
def dfs_helper(self, start_node):
135+
"""TODO: Write the helper function for a recursive implementation
136+
of Depth First Search iterating through a node's edges. The
137+
output should be a list of numbers corresponding to the
138+
values of the traversed nodes.
139+
ARGUMENTS: start_node is the starting Node
140+
MODIFIES: the value of the visited property of nodes in self.nodes
141+
RETURN: a list of the traversed node values (integers).
142+
"""
143+
ret_list = [start_node.value]
144+
# Your code here
145+
start_node.visited = True # set current node to visited
146+
for edge in start_node.edges: # for edge in the list of edges
147+
if edge.node_to.visited == False: # if it hasn't been visited
148+
ret_list = ret_list + self.dfs_helper(edge.node_to) # run this fn on that node, and update list with returned list
149+
return ret_list
150+
151+
def dfs(self, start_node_num):
152+
"""Outputs a list of numbers corresponding to the traversed nodes
153+
in a Depth First Search.
154+
ARGUMENTS: start_node_num is the starting node number (integer)
155+
MODIFIES: the value of the visited property of nodes in self.nodes
156+
RETURN: a list of the node values (integers)."""
157+
self._clear_visited()
158+
start_node = self.find_node(start_node_num)
159+
return self.dfs_helper(start_node)
160+
161+
def dfs_names(self, start_node_num):
162+
"""Return the results of dfs with numbers converted to names."""
163+
return [self.node_names[num] for num in self.dfs(start_node_num)]
164+
165+
def bfs(self, start_node_num):
166+
"""TODO: Create an iterative implementation of Breadth First Search
167+
iterating through a node's edges. The output should be a list of
168+
numbers corresponding to the traversed nodes.
169+
ARGUMENTS: start_node_num is the node number (integer)
170+
MODIFIES: the value of the visited property of nodes in self.nodes
171+
RETURN: a list of the node values (integers)."""
172+
node = self.find_node(start_node_num)
173+
self._clear_visited()
174+
ret_list = []
175+
queue = [node]
176+
# Your code here
177+
while queue:
178+
curr = queue.pop(0)
179+
if curr.visited == False:
180+
ret_list.append(curr.value)
181+
for edge in curr.edges:
182+
node_to = edge.node_to
183+
if node_to.visited == False:
184+
queue.append(node_to)
185+
curr.visited = True
186+
return ret_list
187+
188+
def bfs_names(self, start_node_num):
189+
"""Return the results of bfs with numbers converted to names."""
190+
return [self.node_names[num] for num in self.bfs(start_node_num)]
191+
192+
graph = Graph()
193+
194+
# You do not need to change anything below this line.
195+
# You only need to implement Graph.dfs_helper and Graph.bfs
196+
197+
graph.set_node_names(('Mountain View', # 0
198+
'San Francisco', # 1
199+
'London', # 2
200+
'Shanghai', # 3
201+
'Berlin', # 4
202+
'Sao Paolo', # 5
203+
'Bangalore')) # 6
204+
205+
graph.insert_edge(51, 0, 1) # MV <-> SF
206+
graph.insert_edge(51, 1, 0) # SF <-> MV
207+
graph.insert_edge(9950, 0, 3) # MV <-> Shanghai
208+
graph.insert_edge(9950, 3, 0) # Shanghai <-> MV
209+
graph.insert_edge(10375, 0, 5) # MV <-> Sao Paolo
210+
graph.insert_edge(10375, 5, 0) # Sao Paolo <-> MV
211+
graph.insert_edge(9900, 1, 3) # SF <-> Shanghai
212+
graph.insert_edge(9900, 3, 1) # Shanghai <-> SF
213+
graph.insert_edge(9130, 1, 4) # SF <-> Berlin
214+
graph.insert_edge(9130, 4, 1) # Berlin <-> SF
215+
graph.insert_edge(9217, 2, 3) # London <-> Shanghai
216+
graph.insert_edge(9217, 3, 2) # Shanghai <-> London
217+
graph.insert_edge(932, 2, 4) # London <-> Berlin
218+
graph.insert_edge(932, 4, 2) # Berlin <-> London
219+
graph.insert_edge(9471, 2, 5) # London <-> Sao Paolo
220+
graph.insert_edge(9471, 5, 2) # Sao Paolo <-> London
221+
# (6) 'Bangalore' is intentionally disconnected (no edges)
222+
# for this problem and should produce None in the
223+
# Adjacency List, etc.
224+
225+
import pprint
226+
pp = pprint.PrettyPrinter(indent=2)
227+
228+
print "Edge List"
229+
pp.pprint(graph.get_edge_list_names())
230+
231+
print "\nAdjacency List"
232+
pp.pprint(graph.get_adjacency_list_names())
233+
234+
print "\nAdjacency Matrix"
235+
pp.pprint(graph.get_adjacency_matrix())
236+
237+
print "\nDepth First Search"
238+
pp.pprint(graph.dfs_names(2))
239+
240+
# Should print:
241+
# Depth First Search
242+
# ['London', 'Shanghai', 'Mountain View', 'San Francisco', 'Berlin', 'Sao Paolo']
243+
244+
print "\nBreadth First Search"
245+
pp.pprint(graph.bfs_names(2))
246+
# test error reporting
247+
# pp.pprint(['Sao Paolo', 'Mountain View', 'San Francisco', 'London', 'Shanghai', 'Berlin'])
248+
249+
# Should print:
250+
# Breadth First Search
251+
# ['London', 'Shanghai', 'Berlin', 'Sao Paolo', 'Mountain View', 'San Francisco']

6-graphs/GraphTraversalQuizAnswers.py

Whitespace-only changes.

0 commit comments

Comments
 (0)