Algorithmics study and revision notes
2026-08-22
An exact matcher preprocesses a pattern and scans a text. A full-text index reverses the investment: preprocess one large text once, then answer many future pattern queries without rescanning it. The goal is query time governed mainly by pattern length and output size.
All structures in this note organise the same objects, the text
suffixes, in different representations. The running example
banana$ connects them from first principles.
After studying this note, you should be able to:
You should know tries, binary search, lexicographic order, and asymptotic notation. The succinct-data-structures note introduces bit-vector rank/select; the required rank convention is restated here. The exact-matching note explains why preprocessing choices depend on whether the pattern or text is reused.
The order of the note is a representation ladder. A suffix trie stores every character explicitly; path compression gives a suffix tree; a suffix array stores only lexicographic order and LCP restores shared-prefix information; BWT plus rank turns the same order into backward search; sampling then trades FM-index space against locating time.
Append a sentinel $ that occurs nowhere else and is
ordered before every text symbol. Let the resulting text
include the sentinel. There are
suffixes
,
one from every position
.
For banana$ they are:
| position | suffix |
|---|---|
| 0 | banana$ |
| 1 | anana$ |
| 2 | nana$ |
| 3 | ana$ |
| 4 | na$ |
| 5 | a$ |
| 6 | $ |
Every substring is a prefix of suffix . An index that can navigate suffix prefixes can therefore answer substring queries. The sentinel makes every suffix end at a distinct leaf and gives cyclic-rotation constructions one unambiguous original row.
A suffix trie inserts all suffixes into one character-labelled trie. To search pattern , follow its characters from the root. If the path exists, every descendant leaf stores one occurrence position. Following the path costs with constant-time outgoing-edge lookup. Reporting in another time requires a stored leaf interval or occurrence list; a raw depth-first walk through an uncompressed trie can also traverse long unary paths.
The trie can contain
nodes because the total suffix length is quadratic. A suffix
tree compresses each maximal nonbranching path into one edge.
Store an edge label as the interval [start,end) of the
original text rather than copying its characters.
Search compares pattern characters along edge intervals. If all of is consumed, descendant leaves give its positions. The tree has leaves. Every nonroot internal node has at least two children, so there are fewer than internal nodes; edge intervals and nodes therefore use words with a sparse or fixed-alphabet child representation.
A simple teaching construction inserts every suffix and may take time. Online linear-time constructions maintain suffix links and implicit positions so work is shared between suffixes; Ukkonen’s algorithm is the best-known example. Linear time assumes an alphabet representation supporting the required transition operations. The distinction between linear-size representation and linear-time construction matters.
Suffix trees support more than lookup. An internal node’s string depth is the length of a repeated substring, and its descendant-leaf count is the occurrence count. A generalised suffix tree marks leaves from several texts; the deepest node with leaves from two sources yields a longest common substring.
The suffix array SA[0..N) stores starting positions of
suffixes in lexicographic order. For banana$:
| rank | SA[r] |
suffix |
|---|---|---|
| 0 | 6 | $ |
| 1 | 5 | a$ |
| 2 | 3 | ana$ |
| 3 | 1 | anana$ |
| 4 | 0 | banana$ |
| 5 | 4 | na$ |
| 6 | 2 | nana$ |
Thus SA = [6,5,3,1,0,4,2]. Array values are text
positions; their array indices are suffix ranks.
Materialising and comparison-sorting all suffix strings is simple but uses characters and can spend time in worst-case comparisons. Comparing suffixes by references saves copied space but not repeated character work.
Prefix doubling gives a practical improvement. Initially rank suffixes by their first character. At round , when ranks represent the first characters, sort each suffix position by
using a special rank for positions beyond the sentinel. Rename equal pairs with the same new rank and continue until all ranks differ.
suffix_array_doubling(T):
rank[i] = code(T[i])
length = 1
while length < N:
sort positions i by (rank[i], rank[i+length])
assign new consecutive ranks to unequal pairs
length = 2*length
return positions ordered by final rank
Comparison sorting gives time. Counting/radix sorting integer ranks gives time and working space. Direct linear-time suffix-array algorithms exist, but their engineering cost is higher; an implementation report should name the construction actually measured.
All suffixes beginning with
form one contiguous lexicographic interval. Binary-search the first
suffix not less than
,
then the first suffix greater than every string with prefix
.
The half-open range [lo,hi) contains exactly the
occurrences.
A straightforward comparison examines up to characters at each of search steps, so query time is . Retaining longest-common-prefix information during the searches can avoid repeated comparisons and approach .
Define LCP[0]=0 and, for
,
For banana$, LCP = [0,0,1,3,0,0,2]. The
value 3 between ana$ and anana$ exposes repeat
ana; value 2 between na$ and
nana$ exposes na.
Given SA, its inverse rank array, and the fact that
deleting the first character reduces a known LCP by at most one, Kasai’s
algorithm constructs LCP in
time. Range-minimum queries over LCP give the common-prefix length of
any two suffixes. The suffix array plus LCP and suitable navigation
information can simulate many suffix-tree operations with better memory
locality.
For each suffix-array row, output the character preceding that suffix, wrapping position zero to the sentinel:
For banana$, the rows give L = annb$aa.
This is the BWT. Equivalent definitions sort all cyclic rotations and
take their last column.
The BWT is a reversible permutation of the input, not a compressor by itself. Lexicographically adjacent suffixes have similar following context, so their preceding characters often form runs. Move-to-front, run-length, and entropy coding can exploit those runs; this is why BWT-based compressors can work well.
Let F be the first column of sorted rotations, which is
simply the sorted text. Stable sorting preserves occurrence order: the
th
c in L is the same text occurrence as the
th
c in F.
Use the exclusive convention
and let be the number of text characters strictly smaller than . Then
LF(i) moves from the row for suffix starting at
to the row for the suffix starting at
.
In terms of the inclusive rank convention from the
succinct-data-structures note,
and
for
.
The unique sentinel identifies a starting row for inversion:
invert_bwt(L):
build C and Occ/rank support
row = position of '$' in L
for k = N-1 down to 0:
T[k] = L[row]
row = LF(row)
return T
For annb$aa, the visited output characters in reverse
positions are $, a, n,
a, n, a, b,
reconstructing banana$. With prefix-count tables this is
time; storing a dense Occ table costs
,
while rank data structures reduce space.
Suppose suffix-array interval [l,r) contains exactly the
suffixes beginning with already-processed pattern suffix
.
Prepending character
keeps precisely rows whose preceding BWT character is
.
Stable occurrence ranks map them into the F block for
:
Process the pattern from right to left. Start with every row
[0,N).
anaFor ana in banana$:
| next character | old interval | new interval |
|---|---|---|
a |
[0,7) |
[1,4) |
n |
[1,4) |
[5,7) |
a |
[5,7) |
[2,4) |
The final suffix-array slice is SA[2..4) = [3,1],
exactly the two starts of ana. If an interval becomes
empty, no occurrence exists.
Correctness follows from the LF principle: rows in the old interval
share prefix
,
and selecting their preceding c occurrences gives exactly
suffixes with prefix
.
Their stable ranks make the update endpoints contiguous.
An FM-index stores the BWT with a data structure answering
Occ/rank queries, plus optional samples. Backward search
performs two rank queries per pattern character, so counting takes
For a small alphabet, per-character bit vectors can provide constant-time rank with lower-order redundancy. A wavelet tree or matrix supports larger alphabets, commonly with logarithmic alphabet dependence.
The final interval [l,r) gives count
immediately but not text positions. To locate, start
from each row in the interval. During index construction, sample rows
whose suffix-array value is divisible by period
.
Repeated LF steps decrement the represented suffix position modulo until a sample is reached; at most steps are required under this sampling rule. If steps reach sampled value , the original occurrence starts at .
Samples use about integers, while each reported occurrence costs in the worst case. This is an explicit space–locate-time trade-off.
Compressed-index space claims must include the rank structure and samples. “Near entropy” refers to a particular representation and model; BWT alone is still symbols.
| Structure | Stored space | Basic construction | Exact query |
|---|---|---|---|
| Suffix trie | worst | simple | with a leaf interval |
| Suffix tree | words | simple; advanced | |
| Suffix array | words | prefix doubling with radix sort | basic |
| FM-index | compressed BWT, rank, samples | construction dependent | count plus locate |
SA[r] with the rank of suffix
.Occ.miss$.banana$.banana$ LCP array.LF(0) and LF(4) for
annb$aa.ana.pydivsufsort
provides suffix-array search, BWT and inversion, Kasai LCP, and NumPy
outputs. It accepts Python strings, bytes, and integer arrays; choose
the sequence element that an index position should count instead of
assuming byte and Unicode-character offsets coincide. Its example stores
adjacent LCP values at the left endpoint rather than using this note’s
LCP[r] previous-suffix convention, and its BWT API returns
a primary index instead of requiring the explicit sentinel
representation used here.bio::data_structures::fmindex
exposes byte-oriented BWT, less, sampled Occ,
backward search, and FMD-index components. Its
Occ::occ(r,a) counts through index
inclusively: this note’s
is zero, and for
its
is the library call occ(i-1,a). Backward search returns a
suffix-array interval; the documented SAInterval::occ needs
a suffix array to report text positions, or a separate sampled-SA
locating layer must be supplied.pydivsufsort on banana$ to reproduce
the suffix array, convert its LCP layout to this note’s convention, and
invert its BWT. For each test pattern, compare suffix-array search
positions with a direct scan.