Suffix Trees and Suffix Arrays

Imagine searching a genome three billion characters long for every occurrence of a short DNA motif. Or imagine finding the longest repeated substring in a codebase to detect plagiarism. A naive scan re-reads the text for every query. This costs time proportional to text length times query count. Real systems — genome browsers, full-text search engines, data-compression tools like bzip2 — need to answer thousands of such substring questions against a fixed text. They cannot afford to pay linear-scan cost each time. The suffix tree and suffix array are index structures built once over a text. They are built in time linear or near-linear in its length. Afterward, substring search, longest-common-substring queries, and repeat-finding all run in time proportional to the query size. This is rather than the text size. They encode, implicitly or explicitly, every one of the text's nn suffixes. This encoding exposes shared prefixes as shared structure — a trie compressed into a tree, or an array of suffixes sorted lexicographically. This article develops the suffix array augmented with the longest-common-prefix (LCP) array as the primary, practical structure. It achieves the same asymptotic query power as the suffix tree. It does so with a fraction of the memory footprint — a decisive advantage for large texts. We build the ideas up from the naive O(n2log⁡n)O(n^2 \log n) sort. We move through the classical Manber–Myers doubling algorithm (O(nlog⁡n)O(n \log n)). We continue to the linear-time Kasai algorithm for the LCP array. We connect the whole structure back to the conceptually simpler suffix tree. Building on Knuth-Morris-Pratt String Matching, which answers a single pattern-matching query in linear time against a fixed text scanned once, this article shows how to preprocess the text. This preprocessing lets many pattern queries be answered fast. Structural queries about repeats are also answered fast. It also contrasts index-based search with Introduction to Hashing techniques such as rolling hashes. These techniques trade exactness and worst-case guarantees for simplicity.

Key takeaways
  • A suffix array is the sorted list of starting positions of all nn suffixes of a text; it supports substring search via binary search in O(mlog⁡n)O(m \log n) time.
  • The LCP array records, between consecutive suffixes in sorted order, the length of their longest common prefix — it upgrades the suffix array to answer longest-common-substring and repeat queries.
  • Manber–Myers prefix doubling builds the suffix array in O(nlog⁡n)O(n \log n) time using radix-style rank sorting rounds that double the compared prefix length each pass.
  • Kasai's algorithm computes the LCP array in O(n)O(n) time given the suffix array, by exploiting an amortized argument about how little the matched length can drop between suffixes.
  • A suffix tree stores the same information as a compressed trie of all suffixes; it supports the same and additional queries in optimal time but at roughly 1010–20×20\times the memory of a suffix array.
  • Suffix arrays with LCP information underlie tools such as bzip2's BWT stage, bioinformatics aligners, and full-text search engines that must index gigabyte-scale corpora.
The core insight

Sorting all suffixes of a text turns every substring question into a binary-search or range question over one array, trading a one-time O(nlog⁡n)O(n \log n) build for O(mlog⁡n)O(m \log n) or better queries forever after.

The idea

A suffix of a string SS of length nn is the substring S[i…n−1]S[i \ldots n-1] starting at position ii, for i=0,…,n−1i = 0, \dots, n-1. There are exactly nn suffixes, one per starting index, including the full string itself (i=0i=0) and the single last character (i=n−1i = n-1). The suffix array SASA of SS is a permutation of {0,…,n−1}\{0, \dots, n-1\} listing suffix start positions in lexicographically increasing order of the suffixes they name. To make comparisons total and well-defined, we append a unique sentinel character `‘,smallerthaneverycharacterinthealphabet,guaranteeingnosuffixisaprefixofanotherandthatcomparisonsalwaysterminate.Thekeyinsightisthatanysubstringof`, smaller than every character in the alphabet, guaranteeing no suffix is a prefix of another and that comparisons always terminate. The key insight is that any substring of Sisaprefixofexactlyonesuffix.Consequently,alloccurrencesofaquerypattern is a prefix of exactly one suffix. Consequently, all occurrences of a query pattern Pcorrespondexactlytothesuffixesof correspond exactly to the suffixes of Sthatbeginwith that begin with P—andbecause — and because SAissorted,thosesuffixesoccupyacontiguousrange.Findingalloccurrencesof is sorted, those suffixes occupy a contiguous range. Finding all occurrences of Preducestofindingtherangeofthesortedsuffixlistwhoseentriesstartwith reduces to finding the range of the sorted suffix list whose entries start with P,whichisatextbookbinarysearch:twobinarysearcheslocatethelowerandupperboundofthatrangein, which is a textbook binary search: two binary searches locate the lower and upper bound of that range in O(m \log n)time,where time, where m = |P|.The∗∗LCParray∗∗complements. The **LCP array** complements SA:: \mathrm{LCP}[i]isthelengthofthelongestcommonprefixsharedbythesuffixesat is the length of the longest common prefix shared by the suffixes at SA[i-1]and and SA[i](with (with \mathrm{LCP}[0]undefinedorsetto undefined or set to 0byconvention).Becausesortedordergroupssimilarsuffixestogether,theLCParrayrevealssharedstructuredirectly:thelongestrepeatedsubstringin by convention). Because sorted order groups similar suffixes together, the LCP array reveals shared structure directly: the longest repeated substring in SisthemaximumvalueintheLCParray,andthenumberof∗distinct∗substringsof is the maximum value in the LCP array, and the number of *distinct* substrings of Sequals equals \binom{n}{2}−stylesumsofprefixlengthsminusoverlaps—precisely-style sums of prefix lengths minus overlaps — precisely \sum_i (n - SA[i]) - \mathrm{LCP}[i].Together. Together SAand and LCParefunctionallyequivalenttoa∗∗suffixtree∗∗:acompressedtriewhoseleavesarethe are functionally equivalent to a **suffix tree**: a compressed trie whose leaves are the nsuffixesinleft−to−rightsortedorder,whoseinternalbranchingnodescorrespondexactlytoLCPlocalminima,andwhoseedge−labelconcatenationsfromroottoleaf suffixes in left-to-right sorted order, whose internal branching nodes correspond exactly to LCP local minima, and whose edge-label concatenations from root to leaf ispelloutsuffix spell out suffix SA[i]$. This equivalence is the conceptual bridge: a suffix array is a suffix tree with the tree structure flattened into two arrays, at large memory savings.

How it works

We build the structure in two stages: first sort the suffixes to get SASA, then derive LCPLCP from SASA.

Naive construction. Sorting all nn suffixes directly with a general comparison sort costs O(nlog⁡n)O(n \log n) comparisons, but each comparison between two suffixes can itself take O(n)O(n) time in the worst case (e.g., text `aaaa...a`), giving O(n2log⁡n)O(n^2 \log n) overall — too slow for texts beyond a few thousand characters.

Manber–Myers prefix doubling. The efficient approach sorts suffixes not by their full content at once, but by progressively longer prefixes, doubling the compared length each round: first by 1-character prefixes, then by 2-character prefixes (reusing the 1-character ranks), then 4, then 8, and so on, until the compared prefix length exceeds nn. After ⌈log⁡2n⌉\lceil \log_2 n \rceil rounds, all ties are broken and the ranks give the exact sorted order. The trick that makes each round fast: a suffix's rank by 2k2k-character prefix is determined by the pair (rank-by-kk-prefix of itself, rank-by-kk-prefix of the suffix kk positions later). Sorting these O(n)O(n) pairs by radix sort (or a comparison sort) takes O(nlog⁡n)O(n \log n) or O(n)O(n) per round; with O(log⁡n)O(\log n) rounds the total is O(nlog⁡2n)O(n \log^2 n) with comparison sorting, or O(nlog⁡n)O(n \log n) with radix sorting of the pairs — the latter is the standard bound quoted for this algorithm. ``` function BUILD-SUFFIX-ARRAY(S): n = length(S) append sentinel '$' to S # smaller than all characters n = n + 1

# round 0: rank by single character rank = array of size n for i in 0..n-1: rank[i] = ORD(S[i]) SA = [0, 1, ..., n-1] k = 1 while k < n:

# comparator: compare (rank[i], rank[i+k] or -1) pairs define key(i): first = rank[i] second = rank[i + k] if i + k < n else -1 return (first, second) sort SA by key(·) # O(n log n) comparison sort

# (or O(n) via radix sort on pairs)

# recompute ranks: equal keys get equal rank newRank = array of size n newRank[SA[0]] = 0 for idx in 1..n-1: prevSuffix = SA[idx - 1] currSuffix = SA[idx] if key(currSuffix) == key(prevSuffix): newRank[currSuffix] = newRank[prevSuffix] else: newRank[currSuffix] = newRank[prevSuffix] + 1 rank = newRank if rank[SA[n-1]] == n - 1: break # all suffixes fully distinguished k = k 2 return SA ``` Once SASA is built, <strong>Kasai&#39;s algorithm</strong> computes LCPLCP in linear time by exploiting a monotonicity property: if suffix S[i…]S[i \ldots] has LCP value hh with its predecessor in sorted order, then suffix S[i+1…]S[i+1 \ldots] (the suffix obtained by chopping one character off the front) has LCP at least h−1h - 1 with its predecessor in sorted order. This lets us walk through the text in original index order (not sorted order), maintaining a running match-length counter that only ever decreases by 1 per step, so its total* decrease across the whole pass is bounded by nn even though it may increase arbitrarily at each step. ``` function BUILD-LCP-ARRAY(S, SA): n = length(S) rankOfSuffix = array of size n # inverse permutation of SA for i in 0..n-1: rankOfSuffix[SA[i]] = i LCP = array of size n, all zero h = 0 for i in 0..n-1: if rankOfSuffix[i] > 0: j = SA[rankOfSuffix[i] - 1] # predecessor suffix in sorted order while i + h < n and j + h < n and S[i + h] == S[j + h]: h = h + 1 LCP[rankOfSuffix[i]] = h if h > 0: h = h - 1 else: h = 0 return LCP ``` Substring search against the finished structure uses two binary searches over SASA, comparing the query pattern PP character by character against the suffix starting at each candidate position: ``` function SEARCH-RANGE(S, SA, P): lo = LOWER-BOUND(S, SA, P) # first index with suffix >= P lexicographically hi = UPPER-BOUND(S, SA, P) # first index with suffix > (P followed by max char) if lo == hi: return "not found" return SA[lo .. hi - 1] # all starting positions of occurrences of P ``` Each comparison in the binary search costs up to O(m)O(m) character comparisons, so the whole search costs O(mlog⁡n)O(m \log n). With additional preprocessing (the LCP array combined with the range-minimum-query technique), this bound improves to O(m+log⁡n)O(m + \log n), matching suffix-tree query time.

Figure
Suffix array and LCP array for the text "banana$"
Text S = b a n a n a $ (indices 0..6) index: 0:b 1:a 2:n 3:a 4:n 5:a 6:$ i SA[i] suffix LCP[i] 06$- 15a$0 23ana$1 31anana$3 40banana$0 54na$0 62nana$2 shared prefix "a" shared prefix "ana" Query P = "ana" → binary search on suffix column Matches occupy SA rows 2..3 → occurrences at positions 3 and 1 Longest repeated substring = max(LCP) = 3, at row 3 → "ana" (shared between suffixes starting at 1 and 3: "anana$" and "ana$")

Complexity & correctness

Suffix array construction. The Manber–Myers doubling scheme runs O(log⁡n)O(\log n) rounds (since the compared prefix length doubles from 1 to at least nn). Each round performs a sort of nn rank-pairs. Using a comparison sort this costs O(nlog⁡n)O(n \log n) per round, giving an overall O(nlog⁡2n)O(n \log^2 n); using radix sort on the bounded-range pair keys, each round drops to O(n)O(n), giving the commonly quoted O(nlog⁡n)O(n \log n) total. (Truly linear-time suffix array construction algorithms — DC3/skew, SA-IS — exist and run in O(n)O(n), but they are substantially more intricate; the doubling algorithm is the standard teaching and practical choice, and is what production libraries often ship for moderate-size texts.) Correctness of doubling follows by induction on the round index rr, where the invariant is: after round rr (comparing k=2rk = 2^r-character prefixes), suffixes are correctly grouped and ordered by their first kk characters, with equal-key suffixes tied at equal rank. The base case r=0r=0 holds trivially by definition of the initial rank array (single characters). For the inductive step, two suffixes' first 2k2k characters are compared exactly by the pair (first kk characters of self, first kk characters of the suffix shifted right by kk) — this pair is the rank pair used as sort key, and by the inductive hypothesis the ranks used already correctly order kk-character prefixes. So sorting by these pairs correctly orders 2k2k-character prefixes. The loop terminates once k≥nk \geq n, because with nn distinct suffixes over a text of length nn (plus sentinel), a prefix length of nn characters uniquely distinguishes every suffix — the sentinel guarantees no suffix is a proper prefix of another, so no ties can survive that long.

LCP construction (Kasai). The algorithm runs in O(n)O(n) time. The correctness argument rests on one lemma: if LCP(Si,Si−1’s predecessor)=h\mathrm{LCP}(S_i, S_{i-1\text{'s predecessor}}) = h (in rank terms), then when we move to suffix Si+1S_{i+1} (one character shorter), its LCP with its rank-predecessor is at least h−1h - 1. This holds because removing the first character from both SiS_i and its predecessor SjS_j (which shared hh characters, h≥1h \geq 1) yields Si+1S_{i+1} and Sj+1S_{j+1}, which still share the last h−1h-1 of those characters; and since suffix order is compatible with this shift (a known lexicographic fact: if Sj<SiS_j < S_i and both start with the same character, then Sj+1S_{j+1} and Si+1S_{i+1} remain correctly ordered relative to some suffix no worse in rank distance), the actual predecessor of Si+1S_{i+1} in the array shares at least those h−1h-1 characters. So the match-length variable `h` in the pseudocode never needs to restart from 0; it only decreases by at most 1 per outer-loop iteration, while the inner `while` loop can only increase `h`. Summing increases and decreases: total decreases across the whole run are at most nn (h decreases by at most 1, nn times), so total increases (the work done in all inner-loop iterations combined) are also bounded by O(n)O(n) — this is a standard amortized analysis via a potential function equal to the current value of `h`.

Substring search. Binary search over nn sorted suffixes takes O(log⁡n)O(\log n) comparisons; each suffix-vs-pattern comparison costs up to O(m)O(m) character operations in the worst case, giving O(mlog⁡n)O(m \log n) total — honest and simple, though not optimal. Precomputing, for each binary-search step, the LCP information between the query and known boundary suffixes (a technique using the LCP array plus a sparse table for range-minimum queries) removes redundant character comparisons and achieves O(m+log⁡n)O(m + \log n).

Suffix tree comparison. A suffix tree, built via Ukkonen's algorithm, achieves O(n)O(n) construction time and O(m)O(m) pattern search time (no logarithmic factor) — asymptotically better for search — but at the cost of substantially higher constant-factor memory: each internal node needs child pointers (often via a hash map or fixed-size array over the alphabet), suffix links, and edge-label ranges, typically costing 10–20 bytes per character of text versus roughly 4–8 bytes per character for a suffix array plus LCP array packed as integer arrays. In memory-constrained settings — indexing a multi-gigabase genome, for instance — the suffix array's compactness often outweighs its extra log⁡n\log n query factor, which is why practical bioinformatics tools (e.g., BWA, based on the closely related FM-index/Burrows–Wheeler transform) favor array-based structures.

Honest tradeoffs. The suffix array is not asymptotically optimal for single-pattern search compared to a suffix tree, and building it in true linear time requires the more complex DC3/SA-IS family of algorithms rather than the doubling method shown here. The doubling method's O(nlog⁡n)O(n \log n) bound (with radix sort) is a solid, teachable, and practically fast compromise — but if nn is enormous (billions of characters) and construction time dominates, production systems reach for SA-IS. Also, unlike a hash-based index (see Introduction to Hashing), the suffix array gives exact, worst-case guaranteed bounds with no probability of collision-driven failure, at the cost of the O(log⁡n)O(\log n) or O(nlog⁡n)O(n \log n) overhead hashing usually avoids for simple exact-match lookups — but hashing cannot efficiently answer range queries like "find all suffixes with a given prefix" or "find the longest repeated substring," which is precisely where the sorted suffix structure wins.

Worked example

Take S=banana$S = \texttt{banana\$} (the sentinel `$` is smaller than any letter). Its 7 suffixes, indexed by start position:

start iisuffix
0banana$
1anana$
2nana$
3ana$
4na$
5a$
6$

Trace of prefix doubling (round by round). Round 0 (k=1k=1, sort by 1 character): initial ranks by character: `‘=0,‘a‘=1,‘b‘=2,‘n‘=3.Rankarray(byindex):`=0, `a`=1, `b`=2, `n`=3. Rank array (by index): [2,1,3,1,3,1,0].Sortingindicesbythisrankgives. Sorting indices by this rank gives SA = [6,1,3,5,0,2,4](tiesamongthethree‘a‘−suffixesandtwo‘n‘−suffixesremain,brokenarbitrarilybutconsistentlyatthisstage).∗Round1( (ties among the three `a`-suffixes and two `n`-suffixes remain, broken arbitrarily but consistently at this stage). *Round 1 (k=2,key=(rank[i],rank[i+2])):∗Usinground−0ranks,suffix1("anana, key = (rank[i], rank[i+2])):* Using round-0 ranks, suffix 1 ("anana") has key (rank[1], rank[3]) = (1,1); suffix 3 ("ana")haskey(rank[3],rank[5])=(1,1)—stilltied,correctly,sincebothshareprefix"an".Suffix5("a") has key (rank[3], rank[5]) = (1,1) — still tied, correctly, since both share prefix "an". Suffix 5 ("a") has key (1, rank[7]) = (1, -1) — distinguished from the others since it's shorter. After sorting by these pairs and reassigning ranks, "a"(rankamongthea−group:smallest,since−1sortsfirst)separatesfrom"ana" (rank among the a-group: smallest, since -1 sorts first) separates from "ana" and "anana",whichremaintiedatthisroundbecausetheirfirsttwocharacters"an"agreeandthe∗next∗pairalsocurrentlyties.∗Round2(", which remain tied at this round because their first two characters "an" agree and the *next* pair also currently ties. *Round 2 (k=4):∗nowcomparing4−characterprefixesviapairsbuiltfromround−1ranksresolves"ana):* now comparing 4-character prefixes via pairs built from round-1 ranks resolves "ana" vs "anana"(differingatposition4:"ana" (differing at position 4: "ana" ends, "anana"continues),andseparates"nana" continues), and separates "nana" from "na"similarly.∗Round3(" similarly. *Round 3 (k=8 \geq n=7):∗loopconditionfailstotriggerfurther(rankalreadyfullydistinguishesall7suffixesat):* loop condition fails to trigger further (rank already fully distinguishes all 7 suffixes at k=4$, so the break condition `rank[SA[n-1]] == n-1` fires early) — final:

SA=[6,5,3,1,0,4,2]SA = [6, 5, 3, 1, 0, 4, 2]

corresponding to suffixes: `‘,‘a`, `a`, `ana‘,‘anana`, `anana`, `banana‘,‘na`, `na`, `nana$` — correctly sorted lexicographically (sentinel first, then the `a`-suffixes shortest-to-longest by shared-prefix rule, then `b`, then `n`-suffixes).

Kasai LCP pass, walking i=0,…,6i = 0,\dots,6 in original text order (not sorted order), using `rankOfSuffix` (the inverse of SASA):

ii (text index)rankOfSuffix[i]predecessor j=SA[rank−1]j = SA[\text{rank}-1]matched hhLCP[rank]
04SA[3]=1SA[3]=1compare "banana"vs"anana" vs "anana" → 0LCP[4]=0
13SA[2]=3SA[2]=3compare "anana"vs"ana" vs "ana" → "ana" matches = 3LCP[3]=3
26SA[5]=4SA[5]=4compare "nana"vs"na" vs "na" → "na" = 2LCP[6]=2
32SA[1]=5SA[1]=5compare "ana"vs"a" vs "a" → "a" = 1LCP[2]=1
45SA[4]=0SA[4]=0compare "na"vs"banana" vs "banana" → 0LCP[5]=0
51SA[0]=6SA[0]=6compare "a"vs"" vs "" → 0LCP[1]=0
60none (rank 0)h reset to 0LCP[0]=0

Final LCP=[0,0,1,3,0,0,2]LCP = [0, 0, 1, 3, 0, 0, 2] (indexed by sorted rank, matching the figure above). The maximum LCP value is 3, at rank 3, confirming the longest repeated substring is "ana" — matching the figure's highlighted rows. A query for P=anaP = \texttt{ana} binary-searches SASA and finds the contiguous block at ranks 2–3 (suffixes "ana"and"anana" and "anana"), giving occurrence positions {3,1}\{3, 1\}, exactly as read off the sorted suffix column.

Common pitfalls & traps

PitfallWhy it happensFix
Forgetting the sentinel characterSuffixes can then be prefixes of one another, breaking total order and causing infinite/incorrect comparisonsAlways append a unique symbol smaller than the whole alphabet before building SASA
Off-by-one in LCP indexingLCP[i]\mathrm{LCP}[i] is defined between rank i−1i-1 and rank ii; easy to misalign with rank ii and i+1i+1Fix a single convention (predecessor-based) and use it consistently in both build and query code
Re-scanning characters in binary search naivelyComparing full pattern against full suffix each step gives O(mlog⁡n)O(m \log n), sometimes mistaken for O(log⁡n)O(\log n)State the bound honestly; use LCP + range-minimum-query trick only if the tighter bound is actually needed
Assuming doubling is O(nlog⁡n)O(n \log n) regardless of the inner sortIf each round uses a naive comparison sort instead of radix/counting sort on bounded rank pairs, real cost is O(nlog⁡2n)O(n \log^2 n)Use counting/radix sort on the rank-pair keys to hit the true O(nlog⁡n)O(n\log n) bound
Treating suffix array as always superior to suffix treeSuffix array needs O(log⁡n)O(\log n) (or extra machinery) per query where suffix tree needs O(m)O(m) flatChoose based on memory budget vs. query-time requirements, not by default habit
Ignoring integer overflow / large-alphabet rank arraysRank values grow up to nn; naive fixed-width types can truncate for very large textsUse appropriately sized integer types matched to nn

Pioneers & history

The suffix tree was introduced by Peter Weiner in 1973 as the first linear-time construction of an index encoding all substrings of a text, though his original algorithm was memory-hungry and complex to implement. Edward McCreight simplified the construction in 1976, and Esko Ukkonen gave the now-standard online linear-time construction in 1995, building the tree incrementally character by character — a landmark in string algorithmics for its elegance and its amortized-analysis proof technique. The suffix array, designed explicitly as a space-efficient alternative, was introduced by Udi Manber and Gene Myers in 1990, alongside their O(nlog⁡n)O(n \log n) doubling construction; Kasai, Lee, Arimura, Arikawa, and Park published the linear-time LCP-array construction in 2001, completing the suffix-array toolkit to match suffix-tree query power at a fraction of the memory.

Origins · 1 minUdi Manber and how this was discovered
UM
Udi Manber
born 1955 · Computer scientist and search-systems researcher

Manber, working with Gene Myers at the University of Arizona, co-designed the suffix array specifically to address the practical memory blowup of suffix trees on large genomic and textual datasets, later applying string-indexing ideas at Yahoo! and Google's search infrastructure teams.

History
1973
— Peter Weiner publishes the first linear-time suffix tree construction.
1976
— Edward McCreight gives a simpler, more space-efficient suffix tree algorithm.
1990
— Udi Manber and Gene Myers introduce the suffix array with O(nlog⁡n)O(n \log n) doubling construction.
1995
— Esko Ukkonen publishes the online linear-time suffix tree algorithm.
2001
— Kasai, Lee, Arimura, Arikawa, and Park give the linear-time Kasai LCP algorithm.

Where this matters

Full-text search engines index large static corpora (source-code repositories, legal documents, literary corpora) using suffix-array-style structures to answer substring and phrase queries without scanning the whole corpus per query. Bioinformatics tools for genome alignment (short-read mappers, genome browsers) rely on the closely related Burrows–Wheeler transform and FM-index, both of which are built directly from the suffix array of the reference genome, to align millions of reads per run against multi-gigabase references. Data-compression algorithms such as bzip2 use the Burrows–Wheeler transform — itself computed from a sorted rotation array intimately related to the suffix array — as their core reversible transform before entropy coding. Plagiarism detectors and code-clone finders use the LCP array directly: the longest repeated substring and the longest common substring between two concatenated documents fall out of LCP maxima with almost no extra machinery. Version-control and diff tools, and biological repeat-finding pipelines (finding tandem repeats, transposable elements), similarly lean on suffix structures because they turn an O(n2)O(n^2) pairwise-comparison problem into an O(nlog⁡n)O(n \log n) preprocessing step followed by linear-time repeat enumeration.

:::quiz[What does SA[i] store in a suffix array of length n?]

  • The i-th character of the text
  • The starting index of the i-th lexicographically smallest suffix ✓
  • The length of the i-th suffix
  • The hash value of the i-th suffix

:::quiz[What is LCP[i] conventionally defined as?]

  • The length of the longest common prefix between suffix SA[i] and suffix SA[i+1]
  • The length of the longest common prefix between suffix SA[i-1] and suffix SA[i] ✓
  • The total number of distinct substrings up to position i
  • The number of times character S[i] appears in the text

Ask the tutor: How would you extend a suffix array and LCP array to efficiently find the longest common substring shared across three or more input texts simultaneously?

Why append a sentinel character like `$` to the text?

Without a unique, alphabet-minimum sentinel, one suffix can be a literal prefix of another (e.g., "ana" is a prefix of "anana"). Comparison-based sorting then has no natural way to break the tie, since one string simply runs out of characters mid-comparison. The sentinel guarantees every suffix has a distinct final character and thus a well-defined total order, ensuring the sort terminates correctly and every suffix receives a unique rank. It also ensures no suffix can match past the end of the text during LCP or binary-search comparisons, since the sentinel differs from every real character and stops any spurious match from extending further than intended.

Why not just use a hash table of all substrings instead of a suffix array?

A hash table can answer "does this exact substring exist?" in expected O(1) time after O(n²) preprocessing (or less with rolling hashes per length), but it cannot efficiently answer range-style questions: "what is the longest repeated substring," "list all suffixes starting with this prefix," or "what is the longest common substring between two texts." Those require the ordering information a sorted suffix structure provides. A suffix array also gives worst-case guarantees rather than expected-case guarantees, which matters when adversarial or pathological inputs (many repeated substrings) are possible, since hash-based approaches can degrade under collisions or need per-length hashing to be complete.

How does the suffix array relate to the Burrows–Wheeler transform (BWT)?

The BWT of a text is obtained by sorting all cyclic rotations of the text and taking the last column of the sorted matrix. If the text ends with a sentinel that never repeats, sorting cyclic rotations is equivalent to sorting suffixes, because after the sentinel a rotation just "wraps" with characters that never get compared past it. So the BWT's sorted order is exactly the suffix array's order, and the BWT last-column value at rank i is simply S[SA[i] - 1] (with wraparound). This is why suffix-array construction algorithms are directly reusable to build the BWT, and why tools like bzip2 and the FM-index share so much machinery with suffix-array indexing.

Can the suffix array be built in true linear time, not O(n log n)?

Yes — algorithms such as DC3/skew (Kärkkäinen–Sanders) and SA-IS (Nong, Zhang, Chen) build the suffix array in O(n) time, matching suffix-tree construction asymptotically. They are considerably more intricate: DC3 recursively sorts suffixes at positions ≡1,2 (mod 3) and merges with position-0 suffixes using clever encoding tricks, while SA-IS classifies suffixes into S-type and L-type and induces the full sort from a recursively-sorted subset. The O(n log n) doubling algorithm remains popular in teaching and in many production settings because it is far simpler to implement correctly, and the log n factor is often negligible relative to constant-factor overheads at practical text sizes.

How do you find the longest common substring between two different texts using this structure?

Concatenate the two texts with a unique separator between them (and a sentinel at the end), build one suffix array and LCP array over the concatenation, then scan the LCP array for the maximum value achieved between two suffixes that originate from different original texts (checkable by comparing each suffix's starting index against the length of the first text). This works because any common substring of the two texts must appear as a shared prefix of some suffix from text one and some suffix from text two, and in sorted order such suffixes are necessarily near each other, making their shared-prefix length visible as a (possibly non-adjacent, but boundable) LCP value using range-minimum queries between their ranks.

What is the actual query-time cost of substring search, precisely?

Plain binary search over the suffix array costs O(m log n): each of the O(log n) binary-search steps compares the pattern against a candidate suffix, and each such comparison can take up to O(m) character comparisons in the worst case (when many characters match before a mismatch). Augmenting with the LCP array and a sparse table for range-minimum queries lets each binary-search step reuse previously-computed match lengths, avoiding redundant character comparisons and reducing total search cost to O(m + log n) — but this requires extra O(n log n)-space preprocessing for the RMQ structure, so it is a genuine space-for-speed tradeoff, not a free upgrade.

When should I prefer a suffix tree over a suffix array in practice?

Prefer a suffix tree when query-time optimality matters more than memory — for example, repeated online pattern matching against a moderately sized fixed text where O(m) search (no log factor) meaningfully matters, or when you need suffix links for algorithms like Ukkonen's construction itself or certain tree-based string algorithms (e.g., computing matching statistics, generalized suffix tree multi-string queries). Prefer a suffix array with LCP array when memory is the binding constraint — indexing genome-scale or web-scale text — since it typically uses several times less memory per character while still supporting nearly all the same query types with only a logarithmic-factor query-time penalty.

Before this