Skip to content

Commit 53aa2f4

Browse files
committed
refactor(datastructures, graphs): refactor edge implementation
1 parent dbe61b5 commit 53aa2f4

File tree

12 files changed

+461
-315
lines changed

12 files changed

+461
-315
lines changed

datastructures/graphs/__init__.py

Lines changed: 13 additions & 229 deletions
Original file line numberDiff line numberDiff line change
@@ -1,230 +1,14 @@
1-
from abc import ABC, abstractmethod
2-
from collections import defaultdict
3-
from pprint import PrettyPrinter
4-
from typing import List, Set, Union, Generic, TypeVar
5-
from datastructures.stacks import Stack
61
from .vertex import Vertex
7-
from .edge import Edge
8-
9-
T = TypeVar("T")
10-
11-
12-
class Graph(ABC, Generic[T]):
13-
"""
14-
Represents a Graph Data structure
15-
"""
16-
17-
def __init__(self, edge_list: List[Edge] = None):
18-
if edge_list is None:
19-
edge_list = []
20-
self.edge_list = edge_list
21-
self.adjacency_list = defaultdict(List[Vertex])
22-
self.__construct_adjacency_list()
23-
self.nodes = []
24-
self.node_count = len(self.nodes)
25-
26-
def add(self, node_one: Vertex, node_two: Vertex):
27-
"""
28-
Adds a connection between node_one and node_two
29-
"""
30-
node_one.neighbours.append(node_two)
31-
node_two.neighbours.append(node_one)
32-
edge = Edge(source=node_one, destination=node_two)
33-
self.edge_list.append(edge)
34-
self.adjacency_list[node_one].append(node_two)
35-
self.adjacency_list[node_two].append(node_one)
36-
self.nodes.append(node_one)
37-
self.nodes.append(node_two)
38-
39-
def __construct_adjacency_list(self):
40-
"""
41-
Construct adjacency list
42-
"""
43-
for edge in self.edge_list:
44-
self.adjacency_list[edge.source].append(edge.destination)
45-
46-
@abstractmethod
47-
def bfs_from_root_to_target(self, root: Vertex, target: Vertex) -> Set[Vertex]:
48-
"""
49-
Given the root node to traverse and a target node, returns the BFS result of this Graph from the root node to
50-
the target node
51-
"""
52-
raise NotImplementedError("Not yet implemented")
53-
54-
@abstractmethod
55-
def bfs_from_node(self, source: Vertex) -> Set[Vertex]:
56-
"""
57-
Given the source to traverse, returns the BFS result of this Graph from the source node
58-
"""
59-
raise NotImplementedError("Not yet implemented")
60-
61-
def topological_sorted_order(self) -> List[Vertex]:
62-
"""
63-
Returns the topological sorted order of the Graph
64-
"""
65-
# These static variables are used to perform DFS recursion
66-
# white nodes depict nodes that have not been visited yet
67-
# gray nodes depict ongoing recursion
68-
# black nodes depict recursion is complete
69-
# An edge leading to a BLACK node is not a "cycle"
70-
white = 1
71-
gray = 2
72-
black = 3
73-
74-
# Nothing to do here
75-
if self.node_count == 0:
76-
return []
77-
78-
is_possible = True
79-
stack = Stack()
80-
81-
# By default all nodes are WHITE
82-
visited_nodes = {node: white for node in range(self.node_count)}
83-
84-
def dfs(node: Vertex):
85-
nonlocal is_possible
86-
87-
# Don't recurse further if we found a cycle already
88-
if not is_possible:
89-
return
90-
91-
# start recursion
92-
visited_nodes[node] = gray
93-
94-
# Traverse on neighbouring nodes/vertices
95-
if node in self.adjacency_list:
96-
for neighbour in self.adjacency_list[node]:
97-
if visited_nodes[neighbour] == white:
98-
dfs(node)
99-
elif visited_nodes[node] == gray:
100-
# An Edge to a Gray vertex/node represents a cycle
101-
is_possible = False
102-
103-
# Recursion ends. We mark if as BLACK
104-
visited_nodes[node] = black
105-
stack.push(node)
106-
107-
for node in self.nodes:
108-
# if the node is unprocessed, then call DFS on it
109-
if visited_nodes[node] == white:
110-
dfs(node)
111-
112-
return list(stack.stack) if is_possible else []
113-
114-
def print(self):
115-
pretty_print = PrettyPrinter()
116-
pretty_print.pprint(self.adjacency_list)
117-
118-
def remove(self, node: Vertex) -> None:
119-
"""
120-
Removes all references to a node
121-
:param node
122-
"""
123-
for _, cxns in self.adjacency_list.items():
124-
try:
125-
cxns.remove(node)
126-
except KeyError:
127-
pass
128-
129-
try:
130-
del self.adjacency_list[node]
131-
except KeyError:
132-
pass
133-
134-
def is_connected(self, node_one: Vertex, node_two: Vertex) -> bool:
135-
return (
136-
node_one in self.adjacency_list
137-
and node_two in self.adjacency_list[node_two]
138-
)
139-
140-
def find_path(
141-
self, node_one: Vertex, node_two: Vertex, path=None
142-
) -> Union[List, None]:
143-
"""
144-
Find any path between node_one and node_two. May not be the shortest path
145-
:param node_one
146-
:param node_two
147-
:param path
148-
"""
149-
150-
if path is None:
151-
path = []
152-
path = [path] + [node_one]
153-
154-
if node_one.data == node_two.data:
155-
return path
156-
157-
if node_one.data not in self.adjacency_list:
158-
return None
159-
160-
for node in self.adjacency_list[node_one]:
161-
if node.data not in path:
162-
new_path = self.find_path(node, node_two, path)
163-
164-
if new_path:
165-
return new_path
166-
167-
return None
168-
169-
def find_all_paths(
170-
self, node_one: Vertex, node_two: Vertex, path: List = None
171-
) -> list:
172-
"""
173-
Finds all paths between node_one and node_two, where node_one is the start & node_two is the end
174-
:param node_one Graph Node
175-
:param node_two Graph Node
176-
:param path
177-
"""
178-
if path is None:
179-
path = []
180-
path = path + [node_one]
181-
182-
if node_one.data == node_two.data:
183-
return [path]
184-
185-
if node_one.data not in self.adjacency_list:
186-
return []
187-
188-
paths = []
189-
190-
for node in self.adjacency_list[node_one.data]:
191-
if node not in path:
192-
newpaths = self.find_all_paths(Vertex(node), node_two, path)
193-
for newpath in newpaths:
194-
paths.append(newpath)
195-
196-
return paths
197-
198-
def find_shortest_path(
199-
self, node_one: Vertex, node_two: Vertex, path: List = None
200-
) -> Union[List, None]:
201-
"""
202-
Finds the shortest path between 2 nodes in the graph
203-
"""
204-
if path is None:
205-
path = []
206-
207-
path = path + [node_one]
208-
209-
if node_one.data == node_two.data:
210-
return path
211-
212-
if node_one.data not in self.adjacency_list:
213-
return None
214-
215-
shortest = None
216-
217-
for node in self.adjacency_list[node_one]:
218-
if node.data not in path:
219-
newpath = self.find_shortest_path(node, node_two, path)
220-
if newpath:
221-
if not shortest or len(newpath) < len(shortest):
222-
shortest = newpath
223-
224-
return shortest
225-
226-
def __str__(self):
227-
"""
228-
Return string representation of this Graph
229-
"""
230-
return f"Graph: {self.adjacency_list}"
2+
from .edge import Edge, EdgeType, DirectedEdge, UndirectedEdge, SelfEdge, HyperEdge
3+
from .graph import Graph
4+
5+
__all__ = [
6+
"Edge",
7+
"EdgeType",
8+
"Vertex",
9+
"Graph",
10+
"UndirectedEdge",
11+
"DirectedEdge",
12+
"SelfEdge",
13+
"HyperEdge",
14+
]

datastructures/graphs/edge.py

Lines changed: 0 additions & 54 deletions
This file was deleted.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from .edge import Edge
2+
from .edge_type import EdgeType
3+
from .edge_undirected import UndirectedEdge
4+
from .edge_directed import DirectedEdge
5+
from .edge_self import SelfEdge
6+
from .edge_hyper import HyperEdge
7+
8+
__all__ = [
9+
"Edge",
10+
"EdgeType",
11+
"UndirectedEdge",
12+
"DirectedEdge",
13+
"SelfEdge",
14+
"HyperEdge",
15+
]

datastructures/graphs/edge/edge.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from abc import ABC, abstractmethod
2+
from typing import AnyStr, Union
3+
from .edge_type import EdgeType
4+
from typing import Any, Dict, Optional, Generic, TypeVar, List
5+
from uuid import uuid4
6+
7+
T = TypeVar("T")
8+
9+
class Edge(ABC, Generic[T]):
10+
"""
11+
Edge representation of an abstract Edge in a Graph
12+
"""
13+
14+
def __init__(
15+
self,
16+
weight: Optional[Union[int, float]] = None,
17+
properties: Optional[Dict[str, Any]] = None,
18+
identifier: AnyStr = uuid4(),
19+
):
20+
self.id = identifier
21+
self.weight = weight
22+
self.properties = properties
23+
24+
def __str__(self):
25+
return f"Id: {self.id}, Weight: {self.weight}, Properties: {self.properties}"
26+
27+
@abstractmethod
28+
def edge_type(self) -> EdgeType:
29+
raise NotImplementedError("Not implemented")
30+
31+
def is_unweighted(self) -> bool:
32+
return self.weight is None
33+
34+
@abstractmethod
35+
def vertices(self) -> List[Any]:
36+
raise NotImplementedError("Not implemented")
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from typing import AnyStr, Union, Dict, Optional, Generic, TypeVar, List, Any
2+
from uuid import uuid4
3+
from .edge_type import EdgeType
4+
from .edge import Edge
5+
6+
T = TypeVar("T")
7+
8+
class DirectedEdge(Edge, Generic[T]):
9+
"""
10+
Directed Edge representation of a directed Edge in a Graph where the edge connects two vertices which has a source
11+
vertex and a destination vertex.
12+
"""
13+
14+
def __init__(self, source: Any, destination: Any, weight: Optional[Union[int, float]] = None,
15+
properties: Optional[Dict[str, Any]] = None,
16+
identifier: AnyStr = uuid4()):
17+
super().__init__(weight, properties, identifier)
18+
self.source = source
19+
self.destination = destination
20+
21+
def __str__(self):
22+
return f"{super().__str__()}, Source: {self.source}, Destination: {self.destination}"
23+
24+
def edge_type(self) -> EdgeType:
25+
return EdgeType.DIRECTED
26+
27+
def vertices(self) -> List[Any]:
28+
return [self.source, self.destination]

0 commit comments

Comments
 (0)