Skip to content

Commit 6a662aa

Browse files
sorts: add benchmark_sorts.py comparing algorithms on shared datasets
1 parent e548737 commit 6a662aa

1 file changed

Lines changed: 123 additions & 0 deletions

File tree

sorts/benchmark_sorts.py

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

0 commit comments

Comments
 (0)