Manber-Myers algorithm in Python
Builds the suffix array by prefix doubling in O(n log n) time.
- File
manber_myers_algorithm.py, 50 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/manber-myers-algorithm/. The authors are listed in prov/D2EGK8.jsonl.
from collections.abc import Sequence
from typing import Any
def manber_myers(s: Sequence[Any]) -> list[int]:
"""Return the suffix array of s by prefix doubling with bucket placement.
The alphabet is general ordered: symbols are compared only in the first
sort. No sentinel is appended; positions are 0-based, and a suffix that is
a prefix of another comes first. Time O(n log n) in the worst case, extra
space O(n) integers.
"""
n = len(s)
if n == 0:
return []
sa = sorted(range(n), key=lambda i: s[i]) # @line L1
b = [0] * n # b[i] = the first rank of the bucket of position i
for r in range(1, n): # @line L2
same = s[sa[r]] == s[sa[r - 1]]
b[sa[r]] = b[sa[r - 1]] if same else r
buckets = len(set(b))
h = 1 # @line L3
while h < n and buckets < n: # @line L4
nxt = list(range(n)) # @line L5: nxt[x] = the next free slot of bucket x
new = [0] * n
for i in range(n - 1, n - h - 1, -1): # @line L6: empty second half first
new[nxt[b[i]]] = i
nxt[b[i]] += 1
for r in range(n): # @line L7: the others, in the order of S[j + h ..]
j = sa[r] - h
if j >= 0:
new[nxt[b[j]]] = j
nxt[b[j]] += 1
sa = new # @line L8
def second(i: int) -> int:
return b[i + h] if i + h < n else -1
nb = [0] * n # @line L9: new buckets by (old bucket, second half)
for r in range(1, n):
p, q = sa[r - 1], sa[r]
same = b[p] == b[q] and second(p) == second(q)
nb[q] = nb[p] if same else r
b = nb # @line L10
buckets = len(set(b))
h *= 2
return sa # @line L11
test_manber_myers_algorithm.py 40 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/manber-myers-algorithm/. The authors are listed in prov/D2EGK8.jsonl.
import random
from manber_myers_algorithm import manber_myers
def naive_sa(s: str) -> list[int]:
return sorted(range(len(s)), key=lambda i: s[i:])
def test_example() -> None:
assert manber_myers("mississippi") == [10, 7, 4, 1, 0, 9, 8, 6, 3, 5, 2]
assert manber_myers("banana") == [5, 3, 1, 0, 4, 2]
def test_edges() -> None:
assert manber_myers("") == []
assert manber_myers("z") == [0]
assert manber_myers("aaaaa") == [4, 3, 2, 1, 0]
assert manber_myers("ababab") == [4, 2, 0, 5, 3, 1]
assert manber_myers("ba") == [1, 0]
assert manber_myers([5, 2, 5, 2, 9]) == [1, 3, 0, 2, 4]
def test_long_periodic() -> None:
for s in ("a" * 513, "ab" * 300, "abaababaabaab" * 20):
assert manber_myers(s) == naive_sa(s)
def test_random_against_naive() -> None:
rng = random.Random(1990)
for trial in range(1500):
sigma = "acgt"[: rng.choice([1, 2, 4])]
n = rng.randint(0, 40)
s = "".join(rng.choice(sigma) for _ in range(n))
if trial % 30 == 0:
s = rng.choice(["a" * n, "ab" * (n // 2), ""])
assert manber_myers(s) == naive_sa(s)
What the next level needs
- the tests did not run in this build