Ford-Johnson algorithm in Python
Sorts with the fewest comparisons known for small n; close to the information-theoretic bound.
- File
ford_johnson_algorithm.py, 81 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/ford-johnson-algorithm/. The authors are listed in prov/311MZG.jsonl.
from typing import Generic, Protocol, TypeVar
class Ordered(Protocol):
def __lt__(self, other: "Ordered", /) -> bool: ...
T = TypeVar("T", bound=Ordered)
class _Pair(Generic[T]):
"""A winner a with the loser b of its first comparison; ordered by a."""
__slots__ = ("a", "b")
def __init__(self, a: T, b: T) -> None:
self.a, self.b = a, b
def __lt__(self, other: "_Pair[T]") -> bool:
return self.a < other.a
def _group_ends(m: int) -> list[int]:
"""t_1 = 1, t_2 = 3, t_3 = 5, t_4 = 11, ...: t_k = (2^(k+1) + (-1)^k) / 3, up to m."""
ends = [1]
k = 2
while ends[-1] < m:
ends.append(min(m, (2 ** (k + 1) + (-1) ** k) // 3))
k += 1
return ends
def _merge_insertion(x: list[T]) -> list[T]:
n = len(x)
if n <= 1: # @line L1
return list(x)
pairs: list[_Pair[T]] = []
for i in range(1, n, 2): # @line L2
if x[i] < x[i - 1]: # @line L3
pairs.append(_Pair(x[i - 1], x[i]))
else:
pairs.append(_Pair(x[i], x[i - 1]))
extra = x[-1] if n % 2 == 1 else None # @line L4: the item with no partner
pairs = _merge_insertion(pairs) # @line L5
m = len(pairs)
a = [p.a for p in pairs]
b = [p.b for p in pairs] + ([extra] if extra is not None else [])
s: list[T] = [b[0]] + a # @line L6
ends = _group_ends(len(b))
for k in range(1, len(ends)): # @line L7
for j in range(ends[k], ends[k - 1], -1): # @line L8: b_j for j = t_k down to t_(k-1) + 1
item = b[j - 1]
if j <= m: # @line L9
partner = a[j - 1]
u = next(i for i, v in enumerate(s) if v is partner)
else:
u = len(s)
lo, hi = 0, u # @line L10: binary search in s[0 .. u - 1]
while lo < hi:
mid = (lo + hi) // 2
if item < s[mid]:
hi = mid
else:
lo = mid + 1
s.insert(lo, item)
return s # @line L11
def ford_johnson_algorithm(a: list[T]) -> None:
"""Sort a in place, in non-decreasing order, by merge insertion.
Pair the items, sort the winners recursively, then insert the losers into
the chain of winners by binary search, in the order b3 b2, b5 b4, b11 ... b6,
b21 ... b12, ..., so that each binary search is over at most 2^k - 1 items.
Worst case: F(n) = sum_{k=1..n} ceil(lg(3k/4)) comparisons, which equals
ceil(lg n!) for n <= 11. Not stable.
"""
a[:] = _merge_insertion(a)
test_ford_johnson_algorithm.py 90 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/ford-johnson-algorithm/. The authors are listed in prov/311MZG.jsonl.
import itertools
import math
import random
from ford_johnson_algorithm import ford_johnson_algorithm
class Counted:
count = 0
def __init__(self, k: int) -> None:
self.k = k
def __lt__(self, other: "Counted") -> bool:
Counted.count += 1
return self.k < other.k
def comparisons(p: list[int]) -> int:
a = [Counted(x) for x in p]
Counted.count = 0
ford_johnson_algorithm(a)
assert [x.k for x in a] == sorted(p)
return Counted.count
def worst_case(n: int) -> int:
return sum(math.ceil(math.log2(3 * k / 4)) for k in range(1, n + 1))
def test_example() -> None:
assert comparisons([5, 1, 4, 2, 8, 0, 3]) == 12
a = [5, 1, 4, 2, 8, 0, 3]
ford_johnson_algorithm(a)
assert a == [0, 1, 2, 3, 4, 5, 8]
def test_edges() -> None:
for a in ([], [1], [2, 1], [1, 2], [1, 1, 1], [3, 2, 1], [2**63 - 1, -(2**63)], [0, 2**63 - 1, -(2**63), 0]):
b = list(a)
ford_johnson_algorithm(b)
assert b == sorted(a)
def test_worst_case_is_exact_for_small_n() -> None:
for n in range(0, 9):
worst = max(comparisons(list(p)) for p in itertools.permutations(range(n)))
assert worst == worst_case(n)
assert worst == math.ceil(math.log2(math.factorial(n)))
def test_never_above_the_bound() -> None:
rng = random.Random(1959)
for n in (12, 13, 16, 22, 50, 200):
for _ in range(20):
p = list(range(n))
rng.shuffle(p)
assert comparisons(p) <= worst_case(n)
def test_not_stable() -> None:
class Key:
def __init__(self, k: int, tag: str) -> None:
self.k, self.tag = k, tag
def __lt__(self, other: "Key") -> bool:
return self.k < other.k
a = [Key(0, "a"), Key(1, "b"), Key(0, "c"), Key(0, "d")]
ford_johnson_algorithm(a)
assert [x.k for x in a] == [0, 0, 0, 1]
assert [x.tag for x in a] == ["c", "d", "a", "b"]
def test_random_against_sorted() -> None:
rng = random.Random(1965)
for _ in range(600):
a = [rng.randint(-30, 30) for _ in range(rng.randint(0, 60))]
b = list(a)
ford_johnson_algorithm(b)
assert b == sorted(a)
def test_strings() -> None:
a = ["pear", "fig", "apple", "fig", "kiwi"]
ford_johnson_algorithm(a)
assert a == ["apple", "fig", "fig", "kiwi", "pear"]
What the next level needs
- the tests did not run in this build