mirror of
https://github.com/TheAlgorithms/Python.git
synced 2026-09-28 13:33:11 +08:00
* sorts: add benchmark_sorts.py comparing algorithms on shared datasets * Enhance type safety and clarity in benchmark_sorts.py Updated the benchmark_sorts.py script to improve type safety and added a Comparable protocol for generic sorting. Adjusted comments for clarity and fixed minor formatting issues. --------- Co-authored-by: Christian Clauss <cclauss@me.com>
146 lines
4.6 KiB
Python
146 lines
4.6 KiB
Python
#!/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()
|