Skip to content

Commit e260a26

Browse files
authored
Merge branch 'master' into add-needleman-wunsch
2 parents 8d4a3f2 + 341e1da commit e260a26

1 file changed

Lines changed: 145 additions & 0 deletions

File tree

sorts/benchmark_sorts.py

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
#!/usr/bin/env -S uv run --script
2+
3+
"""
4+
Benchmark several sorting algorithms on the same random datasets.
5+
6+
This is a *reference* benchmark, not a rigorous one: it times each algorithm on a
7+
few shared, randomly generated integer datasets and prints a small comparison
8+
table. It exists so that visitors can see the practical cost of the different
9+
strategies in this directory side by side, without embedding timing code inside
10+
the individual algorithm modules (which keeps those files clean, import-cheap,
11+
and focused on being readable reference implementations).
12+
13+
Run it from the repository root:
14+
15+
python -m sorts.benchmark_sorts
16+
17+
The individual algorithms are imported from their own modules, so this file never
18+
re-implements a sort.
19+
"""
20+
21+
import random
22+
import sys
23+
from collections.abc import Callable, Sequence
24+
from itertools import pairwise
25+
from timeit import timeit
26+
from typing import Protocol
27+
28+
from sorts.bubble_sort import bubble_sort_iterative
29+
from sorts.cocktail_shaker_sort import cocktail_shaker_sort
30+
from sorts.comb_sort import comb_sort
31+
from sorts.gnome_sort import gnome_sort
32+
from sorts.heap_sort import heap_sort
33+
from sorts.insertion_sort import insertion_sort
34+
from sorts.merge_sort import merge_sort
35+
from sorts.quick_sort import quick_sort
36+
from sorts.selection_sort import selection_sort
37+
from sorts.shell_sort import shell_sort
38+
from sorts.tim_sort import tim_sort
39+
40+
# name -> callable. Every callable accepts a list and returns the sorted list.
41+
SORTS: dict[str, Callable[[list[int]], Sequence[int]]] = {
42+
"bubble_sort": bubble_sort_iterative,
43+
"cocktail_shaker_sort": cocktail_shaker_sort,
44+
"comb_sort": comb_sort,
45+
"gnome_sort": gnome_sort,
46+
"heap_sort": heap_sort,
47+
"insertion_sort": insertion_sort,
48+
"merge_sort": merge_sort,
49+
"quick_sort": quick_sort,
50+
"selection_sort": selection_sort,
51+
"shell_sort": shell_sort,
52+
"tim_sort": tim_sort,
53+
}
54+
55+
56+
def is_sorted(collection: Sequence[int]) -> bool:
57+
"""
58+
Return True if every element is less than or equal to the next one.
59+
60+
>>> is_sorted([1, 2, 2, 3])
61+
True
62+
>>> is_sorted([1, 3, 2])
63+
False
64+
>>> is_sorted([])
65+
True
66+
"""
67+
return all(a <= b for a, b in pairwise(collection))
68+
69+
70+
def all_sorts_agree(data: list[int]) -> bool:
71+
"""
72+
Return True if every algorithm in ``SORTS`` sorts ``data`` correctly.
73+
74+
Each algorithm is given a fresh copy of the data (some sort in place), and its
75+
result is checked against Python's built-in ``sorted`` as the ground truth.
76+
77+
>>> all_sorts_agree([5, 1, 4.2, 2, 8.5, 0, 2])
78+
True
79+
>>> all_sorts_agree([])
80+
True
81+
>>> all_sorts_agree([42])
82+
True
83+
>>> all_sorts_agree(list(range(5, -6, -1)))
84+
True
85+
>>> all_sorts_agree(list("Python"))
86+
True
87+
"""
88+
expected = sorted(data)
89+
return all(list(sort_fn(data.copy())) == expected for sort_fn in SORTS.values())
90+
91+
92+
class Comparable(Protocol):
93+
def __lt__(self, other: object, /) -> bool: ...
94+
95+
96+
def benchmark[T: Comparable](data: list[T], number: int = 1) -> dict[str, float]:
97+
"""
98+
Time every algorithm in ``SORTS`` on a copy of ``data``.
99+
100+
Returns a mapping of algorithm name to the elapsed seconds for ``number``
101+
repetitions. Each timed call receives its own fresh copy so in-place sorts do
102+
not hand an already-sorted list to the next repetition.
103+
104+
>>> benchmark([])
105+
Traceback (most recent call last):
106+
...
107+
ValueError: Please provide a non-empty dataset
108+
>>> benchmark([1], number=0)
109+
Traceback (most recent call last):
110+
...
111+
ValueError: Number of repetitions must be positive
112+
"""
113+
if not data:
114+
raise ValueError("Please provide a non-empty dataset")
115+
if number <= 0:
116+
raise ValueError("Number of repetitions must be positive")
117+
timings: dict[str, float] = {}
118+
for name, sort_fn in SORTS.items():
119+
timings[name] = timeit(lambda fn=sort_fn: fn(data.copy()), number=number)
120+
return timings
121+
122+
123+
def main() -> None:
124+
# A couple of the imported algorithms (e.g. tim_sort) merge recursively, so
125+
# give them headroom to sort the largest dataset without hitting the limit.
126+
sys.setrecursionlimit(10_000)
127+
sizes = (100, 1_000, 3_000)
128+
random.seed(0)
129+
datasets = {size: [random.randint(0, size) for _ in range(size)] for size in sizes}
130+
131+
header = "algorithm".ljust(22) + "".join(f"{size:>12}" for size in sizes)
132+
print(header)
133+
print("-" * len(header))
134+
135+
per_size = {size: benchmark(data) for size, data in datasets.items()}
136+
for name in SORTS:
137+
row = name.ljust(22)
138+
row += "".join(f"{per_size[size][name]:>12.4f}" for size in sizes)
139+
print(row)
140+
141+
print("\nseconds per sort (lower is better); dataset = uniform random ints")
142+
143+
144+
if __name__ == "__main__":
145+
main()

0 commit comments

Comments
 (0)