Flashsort in Python
In-place distribution sort that classifies keys into about n/10 classes by a linear map, then permutes and insertion-sorts.
- File
flashsort.py, 93 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/flashsort/. The authors are listed in prov/GEP1AZ.jsonl.
from typing import Callable, Union
Key = Union[int, float, str]
def _common_prefix(s: str, t: str) -> int:
p = 0
while p < len(s) and p < len(t) and s[p] == t[p]:
p += 1
return p
def class_map(a: list[Key], lo: Key, hi: Key, m: int) -> Callable[[Key], int]:
"""Return a non-decreasing map K from the keys of a onto 0 .. m - 1, with K(lo) = 0 and K(hi) = m - 1.
Integers use exact arithmetic, floats use (x - lo) / (hi - lo) with a
clamp, and strings use a code of four characters after the common
prefix of lo and hi. Precondition: lo < hi are the extremes of a.
"""
if isinstance(lo, str):
p = _common_prefix(lo, hi) # type: ignore[arg-type]
base = 2 + max(ord(c) for s in a for c in s[p:p + 4]) # type: ignore[union-attr]
def code(s: str) -> int:
v = 0
for t in range(p, p + 4):
v = v * base + (ord(s[t]) + 1 if t < len(s) else 0)
return v
clo, chi = code(lo), code(hi) # type: ignore[arg-type]
return lambda s: (m - 1) * (code(s) - clo) // (chi - clo) # type: ignore[arg-type]
if isinstance(lo, int) and isinstance(hi, int) and all(isinstance(x, int) for x in a):
span = hi - lo
return lambda x: (m - 1) * (x - lo) // span # type: ignore[operator]
flo, fhi = float(lo) / 2, float(hi) / 2 # halves avoid overflow of hi - lo
fspan = fhi - flo
return lambda x: min(m - 1, max(0, int((float(x) / 2 - flo) / fspan * (m - 1))))
def _insertion_sort(a: list[Key]) -> None:
for i in range(1, len(a)):
x = a[i]
j = i - 1
while j >= 0 and x < a[j]: # type: ignore[operator]
a[j + 1] = a[j]
j -= 1
a[j + 1] = x
def flashsort(a: list[Key], m: int | None = None) -> None:
"""Sort the list a in place, in non-decreasing order. The sort is not stable.
Precondition: the keys are all numbers or all strings; m >= 2 classes,
by default max(2, ceil(n / 10)). The cost is Theta(n + m) expected time
on keys uniform on their range, Theta(n^2) in the worst case, and
Theta(m) extra space.
"""
n = len(a)
if n <= 1: # @line L1
return
lo, hi = min(a), max(a) # @line L2
if not lo < hi: # type: ignore[operator]
return
if m is None:
m = max(2, -(-n // 10))
if m < 2:
raise ValueError("m must be at least 2")
K = class_map(a, lo, hi, m) # @line L3
count = [0] * m # @line L4
for x in a:
count[K(x)] += 1
top = [0] * m # @line L5
s = 0
for k in range(m):
s += count[k]
top[k] = s
i = 0 # @line L6
while i < n: # @line L7
if i >= top[K(a[i])]: # @line L8
i += 1
continue
x = a[i] # @line L9
while True: # @line L10
k = K(x)
top[k] -= 1
p = top[k]
x, a[p] = a[p], x
if p == i:
break
_insertion_sort(a) # @line L11
return # @line L12
test_flashsort.py 64 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/flashsort/. The authors are listed in prov/GEP1AZ.jsonl.
import random
import unittest
from flashsort import class_map, flashsort
class TestFlashsort(unittest.TestCase):
def test_page_example(self):
a = [42, 7, 93, 18, 65, 30, 7, 88, 51, 24]
k = class_map(a, 7, 93, 4)
self.assertEqual([k(x) for x in a], [1, 0, 3, 0, 2, 0, 0, 2, 1, 0])
flashsort(a, m=4)
self.assertEqual(a, [7, 7, 18, 24, 30, 42, 51, 65, 88, 93])
def test_edges(self):
for a in ([], [1], [2, 1], [5, 5, 5], [0, -5, 3, -1, -5, 2], [2**63 - 1, -(2**63), 0, -1, 1]):
b = list(a)
flashsort(b)
self.assertEqual(b, sorted(a))
def test_floats_and_mixed(self):
a = [0.78, 0.17, 0.39, 0.26, 0.72, 0.94, 0.21, 0.12, 0.23, 0.68, 1e308, -1e308]
b = list(a)
flashsort(b)
self.assertEqual(b, sorted(a))
c = [3, 1.5, -2, 0.25]
flashsort(c)
self.assertEqual(c, [-2, 0.25, 1.5, 3])
def test_strings(self):
for a in (["pear", "fig", "apple"], ["b", "", "a", "ab", ""], ["same"] * 5, ["xxxxxxb", "xxxxxxa", "xxxxxx"]):
b = list(a)
flashsort(b)
self.assertEqual(b, sorted(a))
def test_skewed(self):
a = [10**9] + list(range(300, 0, -1))
flashsort(a)
self.assertEqual(a, sorted(a))
def test_bad_m(self):
with self.assertRaises(ValueError):
flashsort([2, 1], m=1)
def test_random(self):
rng = random.Random(1998)
for _ in range(600):
n = rng.randint(0, 80)
kind = rng.randrange(3)
if kind == 0:
a = [rng.randint(-1000, 1000) for _ in range(n)]
elif kind == 1:
a = [rng.random() for _ in range(n)]
else:
a = ["".join(rng.choice("abc") for _ in range(rng.randint(0, 5))) for _ in range(n)]
b = list(a)
flashsort(b, m=rng.choice([None, 2, 3, 50]))
self.assertEqual(b, sorted(a))
if __name__ == "__main__":
unittest.main()
What the next level needs
- the tests did not run in this build