@@ -65,38 +65,72 @@ def topological_sort(graph: dict[int, list[int]]) -> list[int] | None:
6565 return topo_order # valid topological ordering
6666
6767
68+ def _topological_sort_list_queue (graph : dict [int , list [int ]]) -> list [int ] | None :
69+ """
70+ Pre-optimization implementation of Kahn's topological sort using list.pop(0).
71+
72+ Used as a baseline for benchmark comparison against deque.popleft().
73+ """
74+ indegree = [0 ] * len (graph )
75+ queue = []
76+ topo_order = []
77+ processed_vertices_count = 0
78+
79+ for values in graph .values ():
80+ for i in values :
81+ indegree [i ] += 1
82+
83+ for i in range (len (indegree )):
84+ if indegree [i ] == 0 :
85+ queue .append (i )
86+
87+ while queue :
88+ vertex = queue .pop (0 )
89+ processed_vertices_count += 1
90+ topo_order .append (vertex )
91+
92+ for neighbor in graph [vertex ]:
93+ indegree [neighbor ] -= 1
94+ if indegree [neighbor ] == 0 :
95+ queue .append (neighbor )
96+
97+ if processed_vertices_count != len (graph ):
98+ return None
99+ return topo_order
100+
101+
68102def benchmark () -> None :
69103 """
70- Benchmark comparing list.pop(0) vs collections.deque.popleft().
104+ Benchmark comparing topological_sort() (using deque.popleft) against
105+ the pre-optimization baseline _topological_sort_list_queue() (using list.pop(0)).
71106
72- Demonstrates the performance difference between O(n) list.pop(0)
73- and O(1) deque.popleft() operations for Kahn's algorithm queue .
107+ Demonstrates the performance improvement of O(1) queue operations in Kahn's algorithm
108+ on a graph with a large number of zero-indegree vertices .
74109 """
75110 from timeit import timeit
76111
77- size = 50_000
78- runs = 5
112+ num_sources = 30_000
113+ graph = {i : [num_sources ] for i in range (num_sources )}
114+ graph [num_sources ] = []
115+
116+ # Verify correctness: both implementations produce valid topological sorts
117+ old_result = _topological_sort_list_queue (graph )
118+ new_result = topological_sort (graph )
119+ assert old_result is not None and new_result is not None
120+ assert len (old_result ) == len (new_result ) == num_sources + 1
121+ assert set (old_result ) == set (new_result )
79122
80- def use_list () -> None :
81- queue = list (range (size ))
82- while queue :
83- queue .pop (0 )
84-
85- def use_deque () -> None :
86- queue = deque (range (size ))
87- while queue :
88- queue .popleft ()
89-
90- list_time = timeit (use_list , number = runs )
91- deque_time = timeit (use_deque , number = runs )
92-
93- print (f"Benchmark results for queue size of { size } over { runs } runs:" )
94- print (f"list.pop(0): { list_time :.5f} seconds" )
95- print (f"deque.popleft(): { deque_time :.5f} seconds" )
96- if deque_time > 0 :
97- print (
98- f"deque.popleft() is { list_time / deque_time :.2f} x faster than list.pop(0)"
99- )
123+ runs = 5
124+ old_time = timeit (lambda : _topological_sort_list_queue (graph ), number = runs )
125+ new_time = timeit (lambda : topological_sort (graph ), number = runs )
126+
127+ print (
128+ f"Benchmark results for topological_sort with { num_sources } vertices over { runs } runs:"
129+ )
130+ print (f"Pre-optimization (list.pop(0)): { old_time :.5f} seconds" )
131+ print (f"Current (deque.popleft): { new_time :.5f} seconds" )
132+ if new_time > 0 :
133+ print (f"Speedup ratio: { old_time / new_time :.2f} x faster" )
100134
101135
102136if __name__ == "__main__" :
0 commit comments