Algorithm Search⌘K

Larsson-Sadakane algorithm in Python

A faster prefix-doubling suffix sort that skips already sorted groups.

File
larsson_sadakane_algorithm.py, 80 lines
Level
I0 not checked yet
Toolchain
Python 3.14, pytest 9
Test framework
pytest
Run the tests
python3 -m pytest
larsson_sadakane_algorithm.pyRawGitHub
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/larsson-sadakane-algorithm/. The authors are listed in prov/QSTAC1.jsonl.

from collections.abc import Sequence


def _split(sa: list[int], group: list[int], length: list[int], s: int, e: int, h: int) -> None:
    """Sort the group sa[s:e] by the key group[i + h] with ternary-split quicksort.

    Each block of equal keys becomes a new group whose number is its last rank.
    The keys are read before the group changes; the stack replaces recursion.
    """
    n = len(group)
    key = {x: (group[x + h] if x + h < n else -1) for x in sa[s:e]}
    stack = [(s, e)]
    while stack:
        lo, hi = stack.pop()
        if hi - lo == 1:
            group[sa[lo]] = lo
            length[lo] = -1
            continue
        pivot = key[sa[(lo + hi) // 2]]
        part = sa[lo:hi]
        less = [x for x in part if key[x] < pivot]
        equal = [x for x in part if key[x] == pivot]
        more = [x for x in part if key[x] > pivot]
        sa[lo:hi] = less + equal + more
        a, b = lo + len(less), lo + len(less) + len(equal)
        for x in equal:
            group[x] = b - 1
        length[a] = b - a if b - a > 1 else -1
        if more:
            stack.append((b, hi))
        if less:
            stack.append((lo, a))


def larsson_sadakane(text: Sequence) -> tuple[list[int], list[int]]:
    """Return (sa, rank): the suffix array of text and its inverse.

    No sentinel is used: a suffix that is a proper prefix of another sorts first.
    Symbols need a total order. Positions and ranks are 0-based.
    length[s] > 0 marks an unsorted group sa[s : s + length[s]]; length[s] < 0
    marks a run of -length[s] sorted entries, which later passes skip.
    """
    n = len(text)
    if n == 0:  # @line L1
        return [], []
    sa = sorted(range(n), key=text.__getitem__)  # @line L2
    group = [0] * n
    length = [0] * n
    s = 0
    while s < n:
        e = s + 1
        while e < n and text[sa[e]] == text[sa[s]]:
            e += 1
        for r in range(s, e):
            group[sa[r]] = e - 1
        length[s] = e - s if e - s > 1 else -1
        s = e
    h = 1  # @line L3
    while True:  # @line L4
        s, run, unsorted = 0, -1, False
        while s < n:  # @line L5
            if length[s] < 0:  # @line L6
                size = -length[s]
                if run >= 0:
                    length[run] -= size
                else:
                    run = s
                s += size
            else:
                unsorted = True
                e = s + length[s]
                _split(sa, group, length, s, e, h)  # @line L7
                run = -1  # @line L8
                s = e
        if not unsorted:  # @line L9
            return sa, group
        h *= 2  # @line L10
test_larsson_sadakane_algorithm.py 46 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/larsson-sadakane-algorithm/. The authors are listed in prov/QSTAC1.jsonl.

import random

from larsson_sadakane_algorithm import larsson_sadakane


def texts(seed: int, count: int = 800, maxn: int = 30) -> list:
    rng = random.Random(seed)
    out: list = ["", "a", "aa", "ab", "ba"]
    for n in range(1, 20):
        out += ["a" * n, "ab" * n, "aab" * n, "abaab" * (n // 3 + 1)]
    for _ in range(count):
        sigma = rng.choice([1, 2, 4])
        out.append("".join(rng.choice("abcd"[:sigma]) for _ in range(rng.randrange(maxn + 1))))
    return out


def naive_sa(t) -> list[int]:
    return sorted(range(len(t)), key=lambda i: t[i:])


def test_examples() -> None:
    assert larsson_sadakane("banana") == ([5, 3, 1, 0, 4, 2], [3, 2, 5, 1, 4, 0])
    assert larsson_sadakane("mississippi")[0] == [10, 7, 4, 1, 0, 9, 8, 6, 3, 5, 2]
    assert larsson_sadakane("") == ([], [])


def test_against_naive_sort() -> None:
    for t in texts(1):
        sa, rank = larsson_sadakane(t)
        assert sa == naive_sa(t), t
        assert all(rank[sa[r]] == r for r in range(len(t)))


def test_integer_sequences() -> None:
    rng = random.Random(2)
    for _ in range(300):
        t = [rng.randrange(rng.choice([1, 2, 4, 50])) for _ in range(rng.randrange(40))]
        assert larsson_sadakane(t)[0] == naive_sa(t)


def test_long_periodic_text() -> None:
    t = "ab" * 500 + "a"
    assert larsson_sadakane(t)[0] == naive_sa(t)

What the next level needs

  • the tests did not run in this build

Read the full page about Larsson-Sadakane algorithm