Naive Sorting and Its Hidden Cost
A suffix array sorts the starting positions $0,\dots,n-1$ by their suffixes. Direct comparison sorting hides the cost of each key comparison: two repetitive suffixes may share $\Theta(n)$ leading symbols before differing or reaching an end. Thus $O(n\log n)$ key comparisons can require $O(n^2\log n)$ character work. Materializing every suffix also copies $\Theta(n^2)$ symbols.
Prefix-based construction replaces rescanning with integer ranks. Once length-$k$ prefixes have equivalence classes, length-$2k$ prefixes are represented by pairs of class IDs.
Why Naive Sorting Hurts
For $S=\text{"aaaaaa"}$, sorted suffixes are "a", "aa", "aaa", "aaaa", "aaaaa", and "aaaaaa". Adjacent suffixes have LCP lengths $1,2,3,4,5$, so merely establishing adjacent order inspects about $(1+1)+(2+1)+\dots+(5+1)=20$ symbol-or-boundary positions.
For $\text{"a"}^n$, those $n-1$ adjacent checks cost $\sum_{k=1}^{n-1}(k+1)=n(n+1)/2-1=\Theta(n^2)$. A general comparison sort performs additional comparisons, which is why “sort the suffixes” is not an $O(n\log n)$ string algorithm.
Prefix Doubling
Append a unique minimum sentinel. Sorting terminated suffixes is equivalent to sorting cyclic shifts because every rotation contains the sentinel once. Coordinate-compress input symbols to positive integers and reserve rank $0$ for the sentinel.
Suppose class array $C$ ranks cyclic prefixes of length $k$. A length-$2k$ prefix at $i$ is represented by $(C[i],C[(i+k)\bmod N])$. Subtracting $k$ from positions in the existing order already sorts by the second component; stable counting sort by the first class completes the pair sort in $O(N)$.
Scan adjacent pairs to assign new classes, double $k$, and repeat until $k\ge N$ or every class is unique. The $O(\log N)$ rounds cost $O(N)$ each, giving $O(N\log N)$ time and $O(N)$ space.
DC3 and SA-IS Ideas
DC3, or skew, samples positions congruent to $1$ or $2$ modulo $3$, radix-sorts their first three symbols, recursively sorts non-unique names, then sorts modulo-$0$ suffixes and merges using constant-size rank comparisons. The reduced problem has about two-thirds the positions, yielding linear total work.
SA-IS classifies the sentinel suffix as S-type. Position $i$ is S-type when $S[i]\lt S[i+1]$, or when symbols tie and $i+1$ is S-type; otherwise it is L-type. An LMS position is S-type with an L-type predecessor.
Place LMS suffixes at bucket tails, induce L-type suffixes left-to-right into bucket heads, then induce S-type suffixes right-to-left into bucket tails. Name LMS substrings in discovered order. If names are not unique, recursively suffix-sort the reduced name string. Place LMS suffixes in exact order and repeat induction. LMS positions are nonadjacent, so the reduced problem is at most half-sized and the total time is linear for an integer alphabet.
flowchart TD
CLASSIFY["Classify suffixes as L or S"]
LMS["Identify LMS positions"]
PLACE["Place LMS suffixes at bucket tails"]
INDUCE1["Induce L left-to-right, then S right-to-left"]
NAME["Name LMS substrings"]
UNIQUE{"Are names unique?"}
RECURSE["Recursively sort reduced string"]
FINAL["Place LMS suffixes in exact order"]
INDUCE2["Induce final L and S order"]
SA["Completed suffix array"]
CLASSIFY --> LMS
LMS --> PLACE
PLACE --> INDUCE1
INDUCE1 --> NAME
NAME --> UNIQUE
UNIQUE -->|no| RECURSE
UNIQUE -->|yes| FINAL
RECURSE --> FINAL
FINAL --> INDUCE2
INDUCE2 --> SA
Engineering the Construction
The sentinel must be unique and smaller than all real symbols. Coordinate compression with reserved rank $0$ works for Unicode, bytes, tokens, and integer sequences. Memory often dominates: doubling retains order, class, next-class, count, and encoded-text arrays, and language-level integers can multiply the footprint.
Validate that the array has length $n$, contains every position exactly once, and orders adjacent suffixes. Before removal, the sentinel position must be first. Differential-test random small inputs against a naive oracle and include empty, single-symbol, repetitive, sorted, reverse-sorted, and large-alphabet cases.
| Construction | Time bound | Extra space | Alphabet requirement | Implementation difficulty | Practical profile |
|---|---|---|---|---|---|
| Materialized suffix comparison | $O(n^2\log n)$ worst-case character work | $\Theta(n^2)$ | Comparable symbols | Low | Tiny inputs only |
| Index comparison sort | $O(n^2\log n)$ worst case | $O(n)$ indices | Comparable symbols | Low | Small-input oracle |
| Doubling with comparison-sorted pairs | $O(n\log^2 n)$ | $O(n)$ | Initial integer ranks | Moderate | Simple but wastes a logarithmic factor |
| Doubling with radix/counting sort | $O(n\log n)$ | $O(n)$ | Integer-compressible alphabet | Moderate | Strong practical default |
| DC3 / skew | $O(n)$ | $O(n)$ | Linear-time integer radix sort | High | Difference-cover recursion and parallel passes |
| SA-IS | $O(n)$ | $O(n)$ with low-memory variants | Integer alphabet and unique minimum sentinel | High | Excellent at scale; subtle invariants |