CSE3144 · Advanced Data Structures · Jul–Nov Semester 2026 · Lecture 14 of 36 · CO CSE3144.2

Suffix Trees & String Processing

Lecture 13 asked "is this word in my dictionary?" Today's question is different: given one text, answer almost anything about every substring it contains — in time that barely depends on the text's length at all.

Dr. Manu ShrivastavaCourse Instructor · Consultation Fri 2–5 PM, LHC 308F
~70 minutesSession outcome: analyze pattern matching and text indexing methods
L14 · 00 — Agenda ~70 min

Today, part by part

Why suffix trees? One text, every substring question

00–04

Store every suffix of the text in a trie — then any substring is just a prefix search.

The catch: a naive suffix trie can explode to O(m²)

04–08

Verified growth-rate examples — linear for one string family, quadratic for another.

Suffix tree: definition

08–11

Compress every non-branching chain; label edges with index ranges, not copied text.

Advantages and limitations

11–15

O(m) space, O(p) search — at the cost of a genuinely intricate linear-time build.

Animation: building the suffix trie for "banana$"

15–23

All 7 suffixes inserted one at a time — watch reuse climb as the tree fills in.

What a node actually holds

23–27

The same trie node from Lecture 13, plus two integer arrays — nothing new.

Animation: compressing it into the true suffix tree

27–31

23 nodes collapse to 11 — same guarantees, a fraction of the space.

What compression wrote into the root

31–35

The same root array before and after — only child[c] and end[c] ever change.

Animation: pattern matching — three searches, three outcomes

35–43

A multi-occurrence hit, a unique hit, and a clean failure.

Animation: longest repeated substring

35–39

The deepest branching node in the same tree, already built.

Animation: longest common substring — two texts, one tree

39–43

Generalized suffix trees — check all 4 internal nodes, including one decoy.

Complexity, then suffix trie vs. tree vs. array

43–48

Where each structure actually gets used in practice.

Recap & what's next

48–52

Lecture 15: Comparative Analysis of Advanced Tree Structures.

CSE3144 — Lecture 14
L14 · 01 — Why suffix trees? ~4 min

One text, every substring question, answered fast

A different kind of question

Lecture 13's tries answered "is this exact word stored?" against a dictionary of many words. Today's question is different: given one long text T, answer things like "does pattern P occur anywhere in T?", "what's the longest substring that repeats?", "what's the longest stretch shared by two texts?" — over and over, cheaply, without rescanning T each time.

Key trick: every substring of T is a prefix of some suffix of T. So if you store every suffix of T in a trie, answering "does P occur in T?" becomes exactly a prefix search — an operation you already know cold from Lecture 13.

The plan

Take T = "banana", append a unique terminal character $ (smaller than every real character, and appearing nowhere else) to get "banana$" — this guarantees no suffix is ever a prefix of another, so every suffix gets its own distinct leaf. List its 7 suffixes:

banana$, anana$, nana$, ana$, na$, a$, $

Insert all 7 into a trie exactly as in Lecture 13. This structure is called a suffix trie. We'll build it in a few minutes — but first, a serious problem with it.

CSE3144 — Lecture 14
L14 · 02 — The catch ~4 min

A naive suffix trie can cost Θ(m²) space — not Θ(m)

Two strings, two very different growth rates

String "a" repeated m times (aaaa…a): every suffix is a shorter run of a's — the trie is a single chain. Node count: 2m + 2linear in m.

String aⁿbⁿ (say n a's then n b's, so m = 2n): now there are n distinct "a-chains" (one per suffix starting inside the a-run), each needing its own trailing "b-chain" of length up to n. Node count: n² + 4n + 2quadratic in n, i.e. quadratic in m.

Why this happens, and the general bound

In the worst case: the trie's depth (top to bottom) is at most m + 1 — the length of the longest suffix. Its width (how many distinct substrings exist at any one length) can itself be as large as m. Depth × width gives an upper bound of O(m²) nodes — and aⁿbⁿ shows this bound is actually achieved, not just a loose estimate.

For a genome-length text (m in the millions), an O(m²)-space structure is unusable. We need the same lookup power in guaranteed O(m) space — that requirement is exactly what a suffix tree is built to satisfy.

CSE3144 — Lecture 14
L14 · 02a — A concrete look at the two growth rates ~3 min

Same length m = 4, two very different trees: 10 nodes vs. 14

Why the length alone isn't the whole story

Both formulas on the last slide use m — but m by itself doesn't fix the tree's size. What actually decides it is how much the string's own suffixes overlap with each other. Fix m = 4 and compare two genuinely different strings of that exact length.

The two candidates

"aaaa$" — repeated-character family, m = 4 — against "aabb$" — the aⁿbⁿ family with n = 2, so m = 2n = 4. Same length. Watch what happens to the node count.

"aaaa$" — 5 suffixes, maximal sharing

aaaa$, aaa$, aa$, a$, $ — every suffix is a prefix of a longer one, so all 5 sit on one spine, each just peeling off its own "$" leaf. 2(4) + 2 = 10 nodes.

"aabb$" — 5 suffixes, limited sharing

aabb$, abb$, bb$, b$, $ — the two suffixes starting inside the a-run share only their first "a", then diverge, and each needs its own unshared b-tail. 2² + 4(2) + 2 = 14 nodes.

The takeaway

Same m, 40% more nodes — from a string that isn't even trying to be adversarial. Length alone never determines a suffix trie's size; internal repetition does. A highly repetitive string collapses onto one spine and stays linear; a string with only limited self-overlap, even one this short, already shows the quadratic behavior setting in. At genome scale you can't assume the text will be "nice" like aaaa$ — which is exactly why the suffix tree is built to guarantee O(m) no matter which string you hand it.

CSE3144 — Lecture 14
L14 · 03 — Suffix tree: definition ~3 min

Compress every non-branching chain — store index ranges, not text

The compression rule

Take the suffix trie and find every maximal chain of nodes with exactly one child (nodes that carry no real branching decision). Collapse each such chain into a single edge. That edge is labeled with a (start, end) index pair into the original text T — never a copy of the substring itself.

What this guarantees
  • Exactly m leaves for a text of length m (one per suffix, guaranteed distinct by the terminal $).
  • Fewer than m internal branching nodes (excluding the root) — every internal node has ≥ 2 children.
  • Total space: O(m) — always, not just on average. This is the structure called a suffix tree.
CSE3144 — Lecture 14 · Suffix tree definition
L14 · 04 — Advantages & limitations ~4 min

A Swiss-army knife for string problems — with a genuine catch

Advantages
  • Guaranteed O(m) space — no worst-case blow-up, unlike the raw suffix trie.
  • O(p) exact pattern search for any pattern of length p, after one O(m) preprocessing pass over T.
  • One structure answers many different string questions — pattern matching, longest repeated substring, longest common substring of two texts, and more — all in time proportional to the answer, not to rescanning T.
Limitations
  • Construction is intricate. The naive "insert every suffix, then compress" approach we're about to animate costs roughly O(m²) work. A genuinely linear O(m) build is possible, but the algorithm that achieves it is notoriously fiddly to implement without bugs — well beyond what this course derives by hand.
  • Real-world overhead. Each node/edge carries pointers and index pairs — the constant factor per character is larger than a simpler alternative, the suffix array (a sorted array of suffix start-positions plus an LCP array), which is more cache-friendly and is often preferred in production systems despite similar asymptotic guarantees.
CSE3144 — Lecture 14 · Advantages & limitations
L14 · 05 — Animation: building the suffix trie ~8 min

Insert all 7 suffixes of "banana$", one at a time

Exactly the Lecture-13 trie-insertion rule, applied to the 7 suffixes of "banana$" in left-to-right order. Watch how much of each new suffix is brand new versus already there — that ratio is the whole story of why compression will pay off.

Interactive — insert banana$, anana$, nana$, ana$, na$, a$, $
CSE3144 — Lecture 14 · Suffix trie construction
L14 · 05a — What a node actually holds ~4 min

Not a new data structure — a trie node plus two integer arrays

You've just built the trie one character per edge. Before compressing it, look at what a node stores — because compression only works if a single edge can name a whole substring, and that is purely a question of what fits in a node.

Lecture 13's trie node — one array, indexed by character:

struct TrieNode {
    TrieNode* child[26];
    bool      isWord;
}

Slot c holds the subtree under letter c. The edge is that one letter.

Suffix tree node — same array, two integers added per slot:

struct STNode {
    STNode* child[26];
    int     start[26];
    int     end[26];
}

Same slot c, same pointer — plus "this edge spells T[start[c] .. end[c]]".

Two nodes of the trie you just built, laid out in memory

This is still the uncompressed 23-node trie — every edge is one character, so every slot has start[c] = end[c]. The indices point into the original text, which is never copied — it just sits in memory once:

Tbanana$
index1234567
ROOT  (the parent — still the 23-node trie)
slot c$abnc,d,e … z
child[c]LEAF 7NODE A ↓node "b"node "n"NULL
start[c]7213
end[c]7213
reads as"$""a""b""n"
child['a'] points here
NODE A  (the node under slot "a" — same struct, nothing new)
slot c$nevery other letter
child[c]LEAF 6node "an"NULL
start[c]73
end[c]73
reads as"$""n"

So: not a different data structure. It is the same array-indexed-by-character node you built in Lecture 13 — the parent holds pointers, each pointer sits in the slot named by its edge's first character, and the child is just another node of the identical type. The only addition is start[c] and end[c]: two integers that say "this edge spells out T[start..end]" instead of the trie's implicit "this edge spells out exactly the one character c". Right now every slot is at start[c] = end[c] — one character, exactly the trie. But nothing in the struct requires that: two integers hold a 7-character edge just as cheaply as a 1-character one. Widening those end values is all compression does on the next slide, and it is why the node count drops from O(m²) to O(m).

CSE3144 — Lecture 14 · Node structure
L14 · 06 — Animation: compressing into a suffix tree ~4 min

23 nodes become 11 — same guarantees, a fraction of the space

What changes

Every node with exactly one child and no branching decision gets merged into the edge above it. Leaves keep their identity (they're labeled here with the starting position of their suffix — standard suffix-tree convention); only the pure "pass-through" nodes disappear.

How to read the compressed tree

Every edge now carries a full substring label (shown as a small tag on the connecting line) instead of one character. Internal node boxes show the string spelled out from the root to that point — this is exactly the "string-depth" you'll need for the next two animations.

Interactive — before and after
CSE3144 — Lecture 14 · Compression
L14 · 06a — What compression wrote into the root ~4 min

The same root array, before and after the merge

You saw the picture collapse. Here is what actually changed in memory — the root's own array, the one from L14·05a, in both states. The text T is untouched throughout; only the node's three arrays are written.

Before — the uncompressed trie: every edge is one character
ROOT  (23-node trie)
slot c$abnc,d,e … z
child[c]LEAF 7node "a"node "b"node "n"NULL
start[c]7213
end[c]7213
reads as"$""a""b""n"
After — the 11-node suffix tree: amber cells are the only writes
ROOT  (11-node suffix tree)
slot c$abnc,d,e … z
child[c]LEAF 7NODE ALEAF 1NODE CNULL
start[c]7213
end[c]7274
reads as"$""a""banana$""na"
Three things to notice
  • start[c] never moved. Not one slot. An edge always begins where it always began — merging only makes it run further.
  • Only child[c] and end[c] were written. Take the child's end, repoint past the child, free it. Two writes per merge, no matter how long the chain was.
  • "reads as" is not stored. It's computed as T[start..end] whenever the characters are actually needed. There is no fourth array.
Why two slots didn't change at all

Slot $ already pointed straight at a leaf — no chain beneath it to collapse. Slot a pointed at a node that branches immediately (into "$" and "n"), so it carries a real decision and must survive. Only b and n sat above non-branching runs, so only those two slots were touched — exactly the two merges you watched in the animation.

Compare the two end rows: slot b went from 1 to 7, swallowing six nodes into a seven-character edge — and the cost of that label stayed exactly what it was before, two integers. That is the whole reason 23 nodes became 11 with no loss of information.

CSE3144 — Lecture 14 · Node structure after compression
L14 · 07 — Animation: pattern matching ~8 min

Three searches on the same tree — three different outcomes

The rule

Walk from the root, matching the pattern character-by-character against edge labels. Three things can happen: you land exactly on an internal node (pattern occurs — once per leaf below that node); you stop partway along an edge having matched the whole pattern (pattern occurs exactly once — wherever that edge's leaf says); or you hit a mismatch (pattern does not occur at all).

Three searches

Search "ana" — lands exactly on an internal node. Search "ban" — matches fully, but stops partway along one edge. Search "bant" — fails partway along that same edge.

Interactive — trace all three searches
CSE3144 — Lecture 14 · Pattern matching
L14 · 08 — Animation: longest repeated substring ~4 min

The deepest branching node — nothing more to compute

First — what the question actually asks

Given one text T, find the longest string that appears in it at least twice. Not two different texts — one text, comparing it against itself.

For banana, work through the candidates: "a" appears 3× (positions 2, 4, 6). "an" and "na" each appear 2×. "ana" appears 2× — at positions 2 and 4. But "anan" appears only once, and so does "nana". So the answer is "ana", length 3.

One rule that surprises people — overlaps count

Those two copies of "ana" overlap: the first covers positions 2–4, the second covers 4–6, and they share position 4. That still counts as repeating twice.

Tbanana$
at 2ana
at 4ana

Why it matters in practice: repeated blocks are what compression algorithms hunt for, what plagiarism detectors flag, and what biologists look for as repeated motifs in a genome.

Now the tree does all the work. Every internal node spells out a substring that occurs more than once (that's exactly why it branches — two or more different suffixes continue differently from there). So the longest repeated substring is simply whichever internal node is deepest in terms of characters spelled, not tree-edges. No scanning of T, no comparing pairs — just find the deepest branching node.

Interactive — compare the three internal nodes
CSE3144 — Lecture 14 · Longest repeated substring
L14 · 09 — Animation: longest common substring ~5 min

One tree over two texts: the generalized suffix tree

The idea

Append a different unique terminator to each text (say # to S1, $ to S2), then build one suffix tree over the concatenation of both. Label every leaf with which text its suffix came from.

The longest common substring of S1 and S2 is then the deepest internal node whose subtree contains at least one leaf from each text — exactly the same "deepest internal node" idea as longest repeated substring, just with an extra bookkeeping condition.

A small verified example

S1 = abczz, S2 = xyabc. Checking every possible common substring by hand: single characters a, b, c are common; two-character "ab" and "bc" are common; the three-character "abc" is common (S1 positions 1–3, S2 positions 3–5); no four-character substring matches (S1's 4-grams are "abcz","bczz"; S2's are "xyab","yabc" — no overlap).

Longest common substring = "abc", length 3 — found at the deepest node with leaves tagged from both S1 and S2 beneath it.

The 12 suffixes that go into one tree

Two different terminators keep the texts distinguishable — every leaf is tagged with the text it came from and its starting position:

S1 = abczz# — 6 suffixes

abczz#  bczz#  czz#  zz#  z#  #

S2 = xyabc$ — 6 suffixes

xyabc$  yabc$  abc$  bc$  c$  $

Interactive — check every internal node for leaves from both texts
CSE3144 — Lecture 14 · Longest common substring
L14 · 10 — Complexity, then a three-way comparison ~5 min

Same underlying idea, three different structures

OperationCost
Naive build (insert + compress)≈ O(m²) — what we just animated
Linear-time build (not covered here)O(m) — the standard production approach
Exact pattern search (pattern length p)O(p)
Longest repeated substringO(m) — one pass over the already-built tree
Longest common substring (generalized tree)O(m₁ + m₂) — build once, then one pass
AspectSuffix trieSuffix treeSuffix array
SpaceO(m²) worst caseO(m) guaranteedO(m), smallest constant
StructureOne char per edgeCompressed, index-range edgesSorted array of suffix start-positions + LCP array
Build costO(m²) naiveO(m) (linear-time construction)O(m) (modern linear-time array construction)
Ease of implementationSimpleHard — genuinely intricate constructionModerate; very cache-friendly in practice
Typical real useTeaching / tiny alphabets onlyBioinformatics, when tree structure itself is neededSearch engines, genome indices (e.g. BWA, bwt-based tools)
CSE3144 — Lecture 14 · Comparison
L14 · 11 — Recap & what's next ~4 min

Every substring, one tree, no rescanning

Lecture 15 — next

Comparative Analysis of Advanced Tree Structures

Every balanced and indexing structure from Lectures 7–14, laid side by side — when to reach for which one, and why.

Homework — bring to Lecture 15
  • List the 5 suffixes of "abcab$" (including the terminal). Build the naive suffix trie by hand, then compress it — how many nodes remain?
  • On your compressed tree, trace a pattern search for "ab" and for "abz". What does each search return, and why?
  • Find the longest repeated substring of "abcab$" using the deepest-internal-node rule — verify it by directly scanning the string for repeats.
  • In 3–4 sentences: why does appending a unique terminal character like $ matter for guaranteeing exactly m leaves? What could go wrong without it?
  • Reading: Gusfield, Algorithms on Strings, Trees, and Sequences, Ch. 5–7 (suffix trees, linear-time construction, generalized suffix trees).
CSE3144 — Lecture 14

Questions?

Dr. Manu Shrivastava — LHC 308F — Friday 2:00–5:00 PM

Next: Lecture 15 — Comparative Analysis of Advanced Tree Structures.