Algorithm Search⌘K

Proofs: McCreight's algorithm

These are the human proofs of McCreight's algorithm, at full length.

McCreight's algorithm builds the suffix tree in linear time full

This proof shows: correctness, termination, complexity.

Let t=S$t = S\$ with ∣t∣=n+1|t| = n + 1, where $\$ occurs only at the end, and ti=t[i…n]t_i = t[i \ldots n]. For i≥1i \ge 1, headihead_i is the longest prefix of tit_i that is a prefix of some tjt_j, j<ij < i; head0head_0 is the empty string. Since $\$ occurs once, no tit_i is a prefix of an earlier tjt_j (its $\$ would have to be at a position where tjt_j has a symbol of SS), so ∣headi∣<∣ti∣|head_i| < |t_i|. Let Ti\mathcal{T}_i be the compact trie of t0,…,tit_0, \ldots, t_i. A place is a node or a point inside an edge; each prefix of some tjt_j, j≤ij \le i, has exactly one place in Ti\mathcal{T}_i.

Lemma 1. In Ti\mathcal{T}_i, the place of headihead_i is a node (the root, or an inner node with at least two children), and tit_i ends at a leaf whose parent is this node.

Proof. In Ti−1\mathcal{T}_{i-1}, tit_i follows the path of headihead_i and then leaves it, since the next symbol of tit_i does not continue headihead_i in any earlier suffix. Adding tit_i adds one leaf edge at the place of headihead_i, which becomes a node with at least two children (or is the root). □\square

Lemma 2 (McCreight). If headi−1=xαhead_{i-1} = x\alpha with a symbol xx, then α\alpha is a prefix of headihead_i.

Proof. xαx\alpha is a prefix of ti−1t_{i-1} and of some tjt_j, j<i−1j < i - 1. So α\alpha is a prefix of tit_i and of tj+1t_{j+1} with j+1<ij + 1 < i. □\square

Lemma 3. If headi−1=xαhead_{i-1} = x\alpha is an inner node of Ti−1\mathcal{T}_{i-1}, then α\alpha has a place in Ti−1\mathcal{T}_{i-1}; if headi−1head_{i-1} is an inner node of Ti−2\mathcal{T}_{i-2} (it existed before step i−1i - 1), then α\alpha is a node of Ti−2\mathcal{T}_{i-2}, hence of Ti−1\mathcal{T}_{i-1}.

Proof. The first claim is Lemma 2's proof. For the second, an inner node xαx\alpha of Ti−2\mathcal{T}_{i-2} has two children starting with different symbols c≠dc \ne d, so xαcx\alpha c and xαdx\alpha d are prefixes of two suffixes tj,tkt_j, t_k with j,k≤i−2j, k \le i - 2, and αc\alpha c, αd\alpha d are prefixes of tj+1t_{j+1} and tk+1t_{k+1} with j+1,k+1≤i−1j + 1, k + 1 \le i - 1. So α\alpha branches in Ti−1\mathcal{T}_{i-1} and is a node there. Since nodes are never removed and edges are only split, it stays a node in later trees. □\square

Invariant. Before step ii (1≤i≤n1 \le i \le n): the tree is Ti−1\mathcal{T}_{i-1}; hh is the node of headi−1head_{i-1}; every inner node v≠hv \ne h has link(v)link(v) equal to the node of its string without the first symbol; and hh has a link if and only if it is the root or it is an inner node of Ti−2\mathcal{T}_{i-2}.

It holds before step 1: the tree is the root with the leaf of t0t_0, and hh is the root, whose link is the root.

Step ii. Let headi−1=xαhead_{i-1} = x\alpha when hh is not the root.

Case L3, hh is the root. headi−1head_{i-1} is empty, and the scan starts at the root, which is the place of the empty prefix of headihead_i.

Case L4, hh has a link. By the invariant and Lemma 3, link(h)link(h) is the node α\alpha, and α\alpha is a prefix of headihead_i by Lemma 2.

Case L5 to L7, hh has no link. Then hh was created in step i−1i - 1 by a split. Let pp be its parent and β\beta the edge label, so xα=str(p)βx\alpha = \mathrm{str}(p)\beta. If pp is the root, α\alpha is β\beta without its first symbol and the rescan starts at the root. Otherwise p≠hp \ne h has a link to the node of str(p)\mathrm{str}(p) without its first symbol, and α\alpha is that string followed by β\beta. In both cases α\alpha is in the tree (Lemma 3), so the rescan, which at each node takes the child whose edge starts with the next symbol of β\beta and skips the whole edge if it is not longer than the rest of β\beta, reaches the place of α\alpha without comparing other symbols: every edge on the way is on the path of α\alpha, and its symbols agree with α\alpha because α\alpha is in the tree. If the place is inside an edge, the split creates a node there.

Now set link(h)=wlink(h) = w, the node of α\alpha. This is the correct link. If ww was just created by a split, then the place of α\alpha had exactly one continuing symbol cc in Ti−1\mathcal{T}_{i-1}. Since hh was created in step i−1i - 1 by a split at the place where ti−1t_{i-1} left the tree, xαc′x\alpha c' is a prefix of an earlier suffix and xαdx\alpha d a prefix of ti−1t_{i-1} for some c′≠dc' \ne d. Then αc′\alpha c' occurs in Ti−1\mathcal{T}_{i-1}, so c′=cc' = c, and αd\alpha d is a prefix of tit_i with d≠cd \ne c. Hence tit_i leaves the tree right after α\alpha, and headi=αhead_i = \alpha.

Line L8. Otherwise ww is a node of α\alpha, a prefix of headihead_i. The scan compares symbols of tit_i after α\alpha along the tree until the first mismatch, which exists since ∣headi∣<∣ti∣|head_i| < |t_i|; the place reached is the longest prefix of tit_i in Ti−1\mathcal{T}_{i-1}, which is headihead_i. A split there makes it a node.

Lines L9, L10. Adding the leaf of tit_i at the node of headihead_i gives Ti\mathcal{T}_i by Lemma 1. The only inner node created in step ii other than by the rescan split is the node of headihead_i; the rescan split node is headihead_i itself in that case. All older inner nodes, and hh, now have correct links. The new h=headih = head_i has a link exactly when it is the root or an old node. So the invariant holds before step i+1i + 1.

After step nn the tree is Tn\mathcal{T}_n, the compact trie of all suffixes of S$S\$, which is the suffix tree. The loop has nn steps, and each walk goes down a finite tree, so the algorithm terminates.

Time. Write ∣x∣|x| for the string depth and nd(x)nd(x) for the node depth (the number of nodes on the root path, including the root) of a place xx.

Scans. The scan of step ii starts at the node ww of α\alpha, with ∣α∣≥∣headi−1∣−1|\alpha| \ge |head_{i-1}| - 1, and makes at most ∣headi∣−∣α∣+1|head_i| - |\alpha| + 1 symbol comparisons. The sum over ii is at most ∑i(∣headi∣−∣headi−1∣+2)≤∣headn∣+2n≤3n+1\sum_i (|head_i| - |head_{i-1}| + 2) \le |head_n| + 2n \le 3n + 1.

Rescans. For a node vv with a link, nd(link(v))≥nd(v)−1nd(link(v)) \ge nd(v) - 1. Indeed, each ancestor uu of vv other than the root is an inner node with a link, and link(u)link(u) is the node of str(u)\mathrm{str}(u) without its first symbol, which is a prefix of the string of link(v)link(v); so link(u)link(u) is an ancestor of link(v)link(v) or link(v)link(v) itself. Distinct ancestors have distinct string depths, so their links are distinct. This gives nd(v)−1nd(v) - 1 distinct nodes on the root path of link(v)link(v), and nd(link(v))≥nd(v)−1nd(link(v)) \ge nd(v) - 1. The rescan of step ii starts at vv with nd(v)≥nd(p)−1=nd(h)−2nd(v) \ge nd(p) - 1 = nd(h) - 2 and ends at ww, each step going down one node. The scan only goes further down, so the number of rescan steps is at most nd(headi)−nd(headi−1)+2nd(head_i) - nd(head_{i-1}) + 2. The sum is at most nd(headn)+2n≤3n+1nd(head_n) + 2n \le 3n + 1.

Each scan comparison and each rescan step costs one child lookup and O(1)O(1) more work; the remaining work in a step is O(1)O(1). So the total time is O(n)O(n) child lookups, which is O(n)O(n) for a constant alphabet or with hashing, and O(nlog⁡σ)O(n \log \sigma) with balanced child maps. ■\blacksquare

Back to McCreight's algorithm