mirror of
https://github.com/TheAlgorithms/Python.git
synced 2026-09-28 21:45:27 +08:00
* fix: restrict introsort heap fallback to the active range * test: cover introsort heap fallback range boundaries * fix: sort introsort heap fallback ranges without copying * test: cover introsort heap sort range boundaries and default end
241 lines
7.2 KiB
Python
241 lines
7.2 KiB
Python
"""
|
|
Tests for the general-purpose comparison sorts in ``sorts/``.
|
|
|
|
Every algorithm exercised here implements the same contract: given a list of
|
|
mutually comparable items it returns a new list with the same items in
|
|
non-decreasing order (i.e. it agrees with the built-in ``sorted``). Rather than
|
|
repeat a hand-written test per file we run each sort against a shared battery of
|
|
inputs with :func:`pytest.mark.parametrize`.
|
|
|
|
Specialised sorts that only accept a restricted domain are intentionally left
|
|
out (e.g. ``counting_sort``/``radix_sort``/``pigeon_sort`` are integer-only,
|
|
``bead_sort`` needs non-negative integers, ``dutch_national_flag_sort`` expects
|
|
0/1/2, ``bitonic_sort`` needs a power-of-two length, ``topological_sort`` works
|
|
on a graph, and ``stalin_sort``/``wiggle_sort`` deliberately do not fully sort).
|
|
``rec_insertion_sort`` is also left out of the battery: it sorts in place and
|
|
returns ``None`` rather than the sorted collection, so it is exercised
|
|
separately below.
|
|
"""
|
|
|
|
from dataclasses import dataclass
|
|
from typing import NamedTuple
|
|
|
|
import pytest
|
|
|
|
from sorts.binary_insertion_sort import binary_insertion_sort
|
|
from sorts.bogo_sort import bogo_sort
|
|
from sorts.bubble_sort import bubble_sort_iterative, bubble_sort_recursive
|
|
from sorts.circle_sort import circle_sort
|
|
from sorts.cocktail_shaker_sort import cocktail_shaker_sort
|
|
from sorts.comb_sort import comb_sort
|
|
from sorts.cycle_sort import cycle_sort
|
|
from sorts.double_sort import double_sort
|
|
from sorts.exchange_sort import exchange_sort
|
|
from sorts.gnome_sort import gnome_sort
|
|
from sorts.heap_sort import heap_sort
|
|
from sorts.insertion_sort import insertion_sort
|
|
from sorts.intro_sort import heap_sort as intro_heap_sort
|
|
from sorts.intro_sort import intro_sort as intro_sort_range
|
|
from sorts.intro_sort import sort as intro_sort
|
|
from sorts.iterative_merge_sort import iter_merge_sort
|
|
from sorts.merge_insertion_sort import merge_insertion_sort
|
|
from sorts.merge_sort import merge_sort
|
|
from sorts.odd_even_sort import odd_even_sort
|
|
from sorts.odd_even_transposition_single_threaded import odd_even_transposition
|
|
from sorts.pancake_sort import pancake_sort
|
|
from sorts.patience_sort import patience_sort
|
|
from sorts.quick_sort import quick_sort
|
|
from sorts.quick_sort_3_partition import three_way_radix_quicksort
|
|
from sorts.recursive_insertion_sort import rec_insertion_sort
|
|
from sorts.recursive_mergesort_array import merge
|
|
from sorts.reverse_selection import reverse_selection_sort
|
|
from sorts.reversort import reversort
|
|
from sorts.selection_sort import selection_sort
|
|
from sorts.shell_sort import shell_sort
|
|
from sorts.shrink_shell_sort import shell_sort as shrink_shell_sort
|
|
from sorts.smoothsort import smoothsort
|
|
from sorts.stooge_sort import stooge_sort
|
|
from sorts.strand_sort import strand_sort
|
|
from sorts.tim_sort import tim_sort
|
|
from sorts.unknown_sort import merge_sort as unknown_sort
|
|
|
|
|
|
def test_heap_sort() -> None:
|
|
assert heap_sort([]) == []
|
|
assert heap_sort([1]) == [1]
|
|
assert heap_sort([5, 2, 5, 1]) == [1, 2, 5, 5]
|
|
assert heap_sort([1, 2, 3, 4]) == [1, 2, 3, 4]
|
|
assert heap_sort([5, 4, 3, 2, 1]) == [1, 2, 3, 4, 5]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("start", "end"),
|
|
[(start, end) for start in range(7) for end in [None, *range(start, 7)]],
|
|
)
|
|
def test_intro_heap_sort_range(start: int, end: int | None) -> None:
|
|
collection = [100, 4, 1, 3, 1, -100]
|
|
expected = collection[:start] + sorted(collection[start:end])
|
|
if end is not None:
|
|
expected += collection[end:]
|
|
|
|
result = intro_heap_sort(collection, start, end)
|
|
|
|
assert result is collection
|
|
assert collection == expected
|
|
|
|
|
|
@pytest.mark.parametrize("max_depth", [0, 1])
|
|
def test_intro_sort_heap_fallback_preserves_surrounding_items(max_depth: int) -> None:
|
|
collection = [100, *range(40, 0, -1), -100]
|
|
expected = [100, *range(1, 41), -100]
|
|
|
|
result = intro_sort_range(collection, 1, 41, 16, max_depth)
|
|
|
|
assert result is collection
|
|
assert collection == expected
|
|
|
|
|
|
SORTS = (
|
|
binary_insertion_sort,
|
|
bubble_sort_iterative,
|
|
circle_sort,
|
|
cocktail_shaker_sort,
|
|
comb_sort,
|
|
cycle_sort,
|
|
double_sort,
|
|
exchange_sort,
|
|
gnome_sort,
|
|
heap_sort,
|
|
insertion_sort,
|
|
intro_sort,
|
|
iter_merge_sort,
|
|
merge,
|
|
merge_insertion_sort,
|
|
merge_sort,
|
|
odd_even_sort,
|
|
odd_even_transposition,
|
|
pancake_sort,
|
|
patience_sort,
|
|
quick_sort,
|
|
reverse_selection_sort,
|
|
reversort,
|
|
selection_sort,
|
|
shell_sort,
|
|
shrink_shell_sort,
|
|
smoothsort,
|
|
stooge_sort,
|
|
strand_sort,
|
|
three_way_radix_quicksort,
|
|
tim_sort,
|
|
unknown_sort,
|
|
)
|
|
|
|
|
|
@dataclass(order=True)
|
|
class Person:
|
|
name: str = "Bob"
|
|
age: int = 37
|
|
cost: float = 0.0
|
|
|
|
|
|
class Dog(NamedTuple):
|
|
name: str = "Fido"
|
|
age: int = 5
|
|
weight: float = 15.5
|
|
|
|
|
|
CASES = (
|
|
[],
|
|
[1],
|
|
[10, -10, -1, 1, 0],
|
|
[1.1, -1.1, -1, 1, 0],
|
|
list("Python!"),
|
|
[3, 3, 1, 2, 2, 1],
|
|
[5, 4, 3, 2, 1],
|
|
[1, 2, 3, 4, 5],
|
|
[-2, -2, 0, 0, 7, 7],
|
|
[Person(cost=100.0), Person(cost=-100.0), Person(name="Al")],
|
|
[Dog(weight=15.5), Dog(weight=15.1), Dog(name="Buddy")],
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize("sort", SORTS, ids=lambda f: f.__name__)
|
|
@pytest.mark.parametrize("case", CASES, ids=repr)
|
|
def test_sort_matches_builtin(sort, case) -> None:
|
|
"""Each sort must reproduce the ordering of the built-in ``sorted``."""
|
|
assert list(sort(list(case))) == sorted(case)
|
|
|
|
|
|
@pytest.mark.parametrize("case", CASES, ids=repr)
|
|
def test_rec_insertion_sort(case) -> None:
|
|
"""``rec_insertion_sort`` sorts in place and returns ``None``."""
|
|
collection = list(case)
|
|
assert rec_insertion_sort(collection, len(collection)) is None
|
|
assert collection == sorted(case)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"sort",
|
|
[
|
|
binary_insertion_sort,
|
|
bubble_sort_iterative,
|
|
bubble_sort_recursive,
|
|
circle_sort,
|
|
cocktail_shaker_sort,
|
|
comb_sort,
|
|
cycle_sort,
|
|
exchange_sort,
|
|
gnome_sort,
|
|
insertion_sort,
|
|
intro_sort,
|
|
iter_merge_sort,
|
|
merge,
|
|
merge_insertion_sort,
|
|
merge_sort,
|
|
odd_even_sort,
|
|
odd_even_transposition,
|
|
pancake_sort,
|
|
patience_sort,
|
|
reverse_selection_sort,
|
|
reversort,
|
|
selection_sort,
|
|
shrink_shell_sort,
|
|
strand_sort,
|
|
three_way_radix_quicksort,
|
|
tim_sort,
|
|
unknown_sort,
|
|
],
|
|
ids=lambda f: f.__name__,
|
|
)
|
|
def test_sort_rejects_non_comparable_items(sort) -> None:
|
|
with pytest.raises(TypeError):
|
|
sort([1, "a"])
|
|
|
|
|
|
def test_rec_insertion_sort_rejects_non_comparable_items() -> None:
|
|
with pytest.raises(TypeError):
|
|
rec_insertion_sort([1, "a"], 2)
|
|
|
|
|
|
def test_bogo_sort_comparable_items() -> None:
|
|
assert bogo_sort(["c", "a", "b"]) == ["a", "b", "c"]
|
|
assert bogo_sort([2.5, -1.0, 0.0]) == [-1.0, 0.0, 2.5]
|
|
|
|
with pytest.raises(TypeError):
|
|
bogo_sort([1, "a"])
|
|
|
|
|
|
def test_bitonic_sort_comparable_items() -> None:
|
|
from sorts.bitonic_sort import bitonic_sort
|
|
|
|
strings = ["banana", "apple", "cherry", "date"]
|
|
bitonic_sort(strings, 0, len(strings), 1)
|
|
assert strings == ["apple", "banana", "cherry", "date"]
|
|
|
|
numbers = [3, 1.5, 2, 4.5]
|
|
bitonic_sort(numbers, 0, len(numbers), 1)
|
|
assert numbers == [1.5, 2, 3, 4.5]
|
|
|
|
with pytest.raises(TypeError):
|
|
bitonic_sort([1, "two", 3, "four"], 0, 4, 1)
|