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