Bucket sort in Python
Scatters keys into buckets by value range, sorts each bucket, and concatenates; linear expected time on uniform input.
- File
bucket_sort.py, 69 lines- Level
- I0 not checked yet
- Toolchain
- Python 3.14, pytest 9
- Test framework
- pytest
- Run the tests
python3 -m pytest
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/bucket-sort/. The authors are listed in prov/HHT9WC.jsonl.
import math
from typing import Callable, Sequence, Union
Key = Union[int, float, str]
def _prefix_code(s: str, width: int, base: int) -> int:
"""A non-decreasing integer code of the first width characters of s (0 marks the end)."""
code = 0
for i in range(width):
code = code * base + (ord(s[i]) + 1 if i < len(s) else 0)
return code
def bucket_index(a: Sequence[Key], m: int) -> Callable[[Key], int]:
"""Return a non-decreasing map from the keys of a to 0 .. m - 1.
Floats in [0, 1) use floor(m * x), as on the page. Other numbers are
mapped linearly from [min, max]. Strings use a code of their first
four characters, which keeps the lexicographic order (not strictly).
"""
if all(isinstance(x, float) and 0.0 <= x < 1.0 for x in a):
return lambda x: min(math.floor(m * x), m - 1) # type: ignore[operator]
if all(isinstance(x, str) for x in a):
base = 2 + max((ord(c) for s in a for c in s), default=0) # type: ignore[union-attr]
codes = [_prefix_code(s, 4, base) for s in a] # type: ignore[arg-type]
lo, hi = min(codes), max(codes)
return lambda s: (_prefix_code(s, 4, base) - lo) * m // (hi - lo + 1) # type: ignore[arg-type]
if all(isinstance(x, int) for x in a):
lo, hi = min(a), max(a) # type: ignore[type-var]
return lambda x: (x - lo) * m // (hi - lo + 1) # type: ignore[operator]
lo_f, hi_f = float(min(a)), float(max(a)) # type: ignore[type-var,arg-type]
if hi_f == lo_f:
return lambda x: 0
return lambda x: min(math.floor((float(x) - lo_f) / (hi_f - lo_f) * m), m - 1)
def _insertion_sort(b: list[Key]) -> None:
"""Stable insertion sort of the list b, in place."""
for i in range(1, len(b)):
x = b[i]
j = i - 1
while j >= 0 and x < b[j]: # type: ignore[operator]
b[j + 1] = b[j]
j -= 1
b[j + 1] = x
def bucket_sort(a: list[Key]) -> None:
"""Sort the list a in place, in non-decreasing order. The sort is stable.
The keys are numbers or strings. The bucket of x is beta(x), a
non-decreasing map to 0 .. n - 1; for floats in [0, 1) it is
floor(n * x). The cost is Theta(n) expected time on independent
uniform keys, Theta(n^2) in the worst case, and Theta(n) extra space.
"""
n = len(a)
if n <= 1:
return
beta = bucket_index(a, n)
buckets: list[list[Key]] = [[] for _ in range(n)] # @line L1
for i in range(n): # @line L2
buckets[beta(a[i])].append(a[i]) # @line L3
for j in range(n): # @line L4
_insertion_sort(buckets[j]) # @line L5
a[:] = [x for b in buckets for x in b] # @line L6
test_bucket_sort.py 69 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/bucket-sort/. The authors are listed in prov/HHT9WC.jsonl.
import math
import random
from bucket_sort import bucket_index, bucket_sort
def sort_copy(a: list) -> list:
b = list(a)
bucket_sort(b)
return b
def test_example() -> None:
a = [0.78, 0.17, 0.39, 0.26, 0.72, 0.94, 0.21, 0.12, 0.23, 0.68]
beta = bucket_index(a, 10)
assert [beta(x) for x in a] == [7, 1, 3, 2, 7, 9, 2, 1, 2, 6]
assert sort_copy(a) == [0.12, 0.17, 0.21, 0.23, 0.26, 0.39, 0.68, 0.72, 0.78, 0.94]
def test_edges() -> None:
assert sort_copy([]) == []
assert sort_copy([0.5]) == [0.5]
assert sort_copy([0.3, 0.3, 0.3]) == [0.3, 0.3, 0.3]
top = math.nextafter(1.0, 0.0)
assert sort_copy([top, 0.0, top]) == [0.0, top, top]
big = [2**63 - 1, -(2**63), 0, -1, 1, 2**63 - 1]
assert sort_copy(big) == sorted(big)
assert sort_copy(["pear", "", "apple", "ab", "a", "fig"]) == ["", "a", "ab", "apple", "fig", "pear"]
assert sort_copy([2.5, -1.0, 7.25]) == [-1.0, 2.5, 7.25]
def test_rounding_at_the_top() -> None:
lo, hi = -4.286139999468759, 1.3086794732495628
x = math.nextafter(hi, lo)
a = [lo, x] + [lo + (hi - lo) * i / 917 for i in range(917)]
assert sort_copy(a) == sorted(a)
def test_one_bucket_worst_case() -> None:
a = [0.5 + i * 1e-9 for i in range(200, 0, -1)]
assert sort_copy(a) == sorted(a)
def test_stable() -> None:
class Key(float):
pass
a = [Key(0.25), Key(0.25), Key(0.1)]
ids = [id(x) for x in a]
b = list(a)
bucket_sort(b)
assert [id(x) for x in b] == [ids[2], ids[0], ids[1]]
def test_random_against_sorted() -> None:
rng = random.Random(9050)
for _ in range(600):
kind = rng.randrange(3)
m = rng.randint(0, 60)
if kind == 0:
a: list = [rng.random() ** rng.choice([1, 3]) for _ in range(m)]
elif kind == 1:
a = [rng.randint(-50, 50) for _ in range(m)]
else:
a = ["".join(rng.choice("abc") for _ in range(rng.randint(0, 6))) for _ in range(m)]
assert sort_copy(a) == sorted(a)
What the next level needs
- the tests did not run in this build