Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 145 additions & 0 deletions sorts/benchmark_sorts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,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,
}

Check failure on line 53 in sorts/benchmark_sorts.py

View workflow job for this annotation

GitHub Actions / ty

ty (invalid-assignment)

sorts/benchmark_sorts.py:41:58: invalid-assignment: Object of type `dict[str, ((list[int], /) -> Sequence[int]) | ([T](arr: list[T]) -> list[T]) | ([T](lst: list[T]) -> list[T]) | ([T](unsorted: list[T]) -> list[T]) | ([T](collection: list[T]) -> list[T])]` is not assignable to `dict[str, (list[int], /) -> Sequence[int]]` sorts/benchmark_sorts.py:41:8: Declared type info: element `[T](arr: list[T]) -> list[T]` of union `((list[int], /) -> Sequence[int]) | ([T](arr: list[T]) -> list[T]) | ([T](lst: list[T]) -> list[T]) | ([T](unsorted: list[T]) -> list[T]) | ([T](collection: list[T]) -> list[T])` is not assignable to `(list[int], /) -> Sequence[int]` info: └── the first parameter has an incompatible type: `list[int]` is not assignable to `list[T@cocktail_shaker_sort]` info: └── type `int` is not assignable to protocol `Comparable` info: └── protocol member `__lt__` is incompatible info: └── parameter `other` has an incompatible type: `object` is not assignable to `int`


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
Comment thread
cclauss marked this conversation as resolved.
>>> 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: ...

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As there is no test file in this pull request nor any test function or class in the file sorts/benchmark_sorts.py, please provide doctest for the function __lt__

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LOL!!!



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
"""
Comment thread
cclauss marked this conversation as resolved.
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:
Comment thread
cclauss marked this conversation as resolved.
Comment thread
cclauss marked this conversation as resolved.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As there is no test file in this pull request nor any test function or class in the file sorts/benchmark_sorts.py, please provide doctest for the function main

# 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()
Loading