Skip to content

Commit 54e0eed

Browse files
committed
Fix the DFS implementation of Topological Sort, add docstrings, doctests, and comments
1 parent c3d4b9e commit 54e0eed

File tree

1 file changed

+114
-38
lines changed

1 file changed

+114
-38
lines changed

sorts/topological_sort.py

Lines changed: 114 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,117 @@
1-
"""Topological Sort."""
2-
3-
# a
4-
# / \
5-
# b c
6-
# / \
7-
# d e
8-
edges: dict[str, list[str]] = {
9-
"a": ["c", "b"],
10-
"b": ["d", "e"],
11-
"c": [],
12-
"d": [],
13-
"e": [],
14-
}
15-
vertices: list[str] = ["a", "b", "c", "d", "e"]
16-
17-
18-
def topological_sort(start: str, visited: list[str], sort: list[str]) -> list[str]:
19-
"""Perform topological sort on a directed acyclic graph."""
20-
current = start
21-
# add current to visited
22-
visited.append(current)
23-
neighbors = edges[current]
24-
for neighbor in neighbors:
25-
# if neighbor not in visited, visit
26-
if neighbor not in visited:
27-
sort = topological_sort(neighbor, visited, sort)
28-
# if all neighbors visited add current to sort
29-
sort.append(current)
30-
# if all vertices haven't been visited select a new one to visit
31-
if len(visited) != len(vertices):
32-
for vertice in vertices:
33-
if vertice not in visited:
34-
sort = topological_sort(vertice, visited, sort)
35-
# return sort
36-
return sort
1+
"""
2+
Topological sorting for Directed Acyclic Graph (DAG) is an ordering of vertices
3+
such that for every directed edge u -> v, vertex u comes before v in the ordering.
4+
Source: https://en.wikipedia.org/wiki/Topological_sorting#Depth-first_search
5+
"""
6+
7+
8+
def topological_sort(graph: dict[str, list[str]]) -> list[str]:
9+
"""
10+
Performs topological sorting on a directed acyclic graph (DAG).
11+
12+
Args:
13+
graph: A dictionary representing the adjacency lists of the graph
14+
where keys are nodes and values are lists of adjacent nodes.
15+
16+
Returns:
17+
A list of nodes in topologically sorted order.
18+
19+
Raises:
20+
ValueError: If the graph contains a cycle (topological sort not possible).
21+
22+
Examples:
23+
>>> # Simple linear graph
24+
>>> # 1 -> 2 -> 3
25+
>>> topological_sort({'1': ['2'], '2': ['3'], '3': []})
26+
['1', '2', '3']
27+
28+
>>> # Graph with multiple possible orderings
29+
>>> # A
30+
>>> # ↙ ↘
31+
>>> # B C
32+
>>> # ↙ ↘
33+
>>> # D E
34+
>>> graph = {
35+
... 'A': ['B', 'C'],
36+
... 'B': ['D', 'E'],
37+
... 'C': [],
38+
... 'D': [],
39+
... 'E': []
40+
... }
41+
>>> import random
42+
>>> adjacency_lists = list(graph.items())
43+
>>> random.shuffle(adjacency_lists)
44+
>>> graph = dict(adjacency_lists)
45+
>>> result = topological_sort(graph)
46+
>>> result in (
47+
... ['A', 'B', 'C', 'D', 'E'],
48+
... ['A', 'B', 'C', 'E', 'D'],
49+
... ['A', 'B', 'D', 'C', 'E'],
50+
... ['A', 'B', 'D', 'E', 'C'],
51+
... ['A', 'B', 'E', 'C', 'D'],
52+
... ['A', 'B', 'E', 'D', 'C'],
53+
... ['A', 'C', 'B', 'D', 'E'],
54+
... ['A', 'C', 'B', 'E', 'D']
55+
... )
56+
True
57+
58+
>>> # Empty graph
59+
>>> topological_sort({})
60+
[]
61+
62+
>>> # Graph with cycle (should raise error)
63+
>>> topological_sort({'A': ['B'], 'B': ['C'], 'C': ['A']})
64+
Traceback (most recent call last):
65+
...
66+
ValueError: Graph contains a cycle, topological sort not possible
67+
"""
68+
69+
is_being_visited = set() # To track nodes in current path being visited through DFS
70+
visited = set() # To track and efficiently lookup if a node has been fully visited
71+
result = []
72+
73+
def visit(node: str) -> None:
74+
75+
is_being_visited.add(node)
76+
77+
for neighbor in graph.get(node, []):
78+
79+
if neighbor in visited:
80+
continue
81+
82+
if neighbor in is_being_visited:
83+
# If the 'neighbor' is already in 'is_being_visited',
84+
# it means we have found a cycle.
85+
raise ValueError(
86+
"Graph contains a cycle, topological sort not possible"
87+
)
88+
89+
visit(neighbor)
90+
91+
is_being_visited.remove(node)
92+
visited.add(node)
93+
# Fully visited nodes are supposed to be prepended to the result list
94+
# But since prepending to python lists is O(n), i.e., costly,
95+
# we will append them and reverse the result at the end.
96+
result.append(node)
97+
98+
for node in graph:
99+
if node not in visited:
100+
visit(node)
101+
102+
return result[::-1] # Reverse the result to get the correct topological order
37103

38104

39105
if __name__ == "__main__":
40-
sort = topological_sort("a", [], [])
41-
print(sort)
106+
107+
graph: dict[str, list[str]] = {
108+
"A": ["B", "C"],
109+
"B": ["D", "E"],
110+
"C": [],
111+
"D": [],
112+
"E": [],
113+
}
114+
115+
sorted_values = topological_sort(graph)
116+
117+
print(f"Sorted values: {sorted_values}")

0 commit comments

Comments
 (0)