Data Structures, Algorithms, & Applications in Java
Have You Seen This String?
Suffix Trees
Copyright 1999 Sartaj Sahni
The Suffix Tree
Let's Find That Substring
Other Nifty Things You Can Do with a Suffix Tree
How to Build Your Very Own Suffix Tree
Exercises
References and Selected Readings
Have You Seen This String?
In the classical substring search problem, we are given a stringS
and a patternP
and are to report whether or not the patternP
occurs in the stringS
. For example, the patternP = cat
appears (twice) in the stringS1 = The big cat ate the small catfish.
, but does not appear in the stringS2 = Dogs for sale.
.
Researchers in the human genome project are constantly searching for substrings/patterns (we use the terms substring and pattern interchangeably) in a gene databank that contains tens of thousands of genes. Each gene is represented as a sequence or string of letters drawn from the alphabet{A, C, G, T}
. Although, most of the strings in the databank are around2000
letters long, some have tens of thousands of letters. Because of the size of the gene databank and the frequency with which substring searches are done, it is imperitive that we have as fast an algorithm as possible to locate a given substring within the strings in the databank.
We can search for a patternP
in a stringS
, using pattern matching algorithms that are described in standard algorithm's texts. The complexity of such a search isO(|P| + |S|)
, where|P|
denotes the length (i.e., number of letters or digits) ofP
. This complexity looks pretty good when you consider that the patternP
could appear anywhere in the stringS
. Therefore, we must examine every letter/digit (we use the terms letter and digit interchangeably) of the string before we can conclude that the search pattern does not appear in the string. Further, before we can conclude that the search pattern appears in the string, we must examine every digit of the pattern. Hence, every pattern search algorithm must take time that is linear in the lengths of the pattern and the string being searched.
When classical pattern matching algorithms are used to search for several patternsP1, P2, ..., Pk
in the stringS
,O(|P1| + |P2| + ... + |Pk| + k|S|)
time is taken (becauseO(|Pi| + |S|)
time is taken to seach forPi
). The suffix tree data structure that we are about to study reduces this complexity toO(|P1| + |P2| + ... + |Pk| + |S|)
. Of this time,O(|S|)
time is spent setting up the suffix tree for the stringS
; an individual pattern search takes onlyO(|Pi|)
time (after the suffix tree forS
has been built). Therefore once the suffix tree forS
has been created, the time needed to search for a pattern depends only on the length of the pattern.
The Suffix Tree
The suffix tree forS
is actually the compressed trie for the nonempty suffixes of the stringS
. Since a suffix tree is a compressed trie, we sometimes refer to the tree as a trie and to its subtrees as subtries.
The (nonempty) suffixes of the stringS = peeper
arepeeper, eeper, eper, per, er,
andr
. Therefore, the suffix tree for the stringpepper
is the compressed trie that contains the elements (which are also the keys)peeper, eeper, eper, per, er,
andr
. The alphabet for the stringpeeper
is{e, p, r}
. Therefore, the radix of the compressed trie is3
. If necessary, we may use the mappinge -> 0, p -> 1, r -> 2
, to convert from the letters of the string to numbers. This conversion is necessary only when we use a node structure in which each node has an array of child pointers. Figure 1 shows the compressed trie (with edge information) for the suffixes ofpeeper
. This compressed trie is also the suffix tree for the stringpeeper
.
Figure 1 Compressed trie for the suffixes of peeper
Since the data in the information nodesD-I
are the suffixes ofpeeper
, each information node need retain only the start index of the suffix it contains. When the letters inpeeper
are indexed from left to right beginning with the index1
, the information nodesD-I
need only retain the indexes6, 2, 3, 5, 1,
and4
, respectively. Using the index stored in an information node, we can access the suffix from the stringS
. Figure 2 shows the suffix tree of Figure 1 with each information node containing a suffix index.
Figure 2 Modified compressed trie for the suffixes of peeper
The first component of each branch node is a reference to an element in that subtrie. We may replace the element reference by the index of the first digit of the referenced element. Figure 3 shows the resulting compressed trie. We shall use this modified form as the representation for the suffix tree.
Figure 3 Suffix tree for peeper
When describing the search and construction algorithms for suffix trees, it is easier to deal with a drawing of the suffix tree in which the edges are labeled by the digits used in the move from a branch node to a child node. The first digit of the label is the digit used to determine which child is moved to, and the remaining digits of the label give the digits that are skipped over. Figure 4 shows the suffix tree of Figure 3 drawn in this manner.
Figure 4 A more humane drawing of a suffix tree
In the more humane drawing of a suffix tree, the labels on the edges on any root to information node path spell out the suffix represented by that information node. When the digit number for the root is not1
, the humane drawing of a suffix tree includes a head node with an edge to the former root. This edge is labeled with the digits that are skipped over.
The string represented by a node of a suffix tree is the string spelled by the labels on the path from the root to that node. NodeA
of Figure 4 represents the empty stringepsilon
, nodeC
represents the stringpe
, and nodeF
represents the stringeper
.
Since the keys in a suffix tree are of different length, we must ensure that no key is a proper prefix of another (see Keys With Different Length). Whenever the last digit of stringS
appears only once inS
, no suffix ofS
can be a proper prefix of another suffix ofS
. In the stringpeeper
, the last digit isr
, and this digit appears only once. Therefore, no suffix ofpeeper
is a proper prefix of another. The last digit ofdata
isa
, and this last digit appears twice indata
. Therefore,data
has two suffixesata
anda
that begin witha
. The suffixa
is a proper prefix of the suffixata
.
When the last digit of the stringS
appears more than once inS
we must append a new digit (say#
) to the suffixes ofS
so that no suffix is a prefix of another. Optionally, we may append the new digit toS
to get the stringS#
, and then construct the suffix tree forS#
. When this optional route is taken, the suffix tree has one more suffix (#
) than the suffix tree obtained by appending the symbol#
to the suffixes ofS
.
Let's Find That Substring
But First, Some Terminology
Letn = |S|
denote the length (i.e., number of digits) of the string whose suffix tree we are to build. We number the digits ofS
from left to right beginning with the number1
.S[i]
denotes thei
th digit ofS
, andsuffix(i)
denotes the suffixS[i]...S[n]
that begins at digiti
,1 <= i <= n
.
On With the Search
A fundamental observation used when searching for a patternP
in a stringS
is thatP
appears inS
(i.e.,P
is a substring ofS
) iffP
is a prefix of some suffix ofS
.
Suppose thatP = P[1]...P[k] = S[i]...S[i+k-1]
. Then,P
is a prefix ofsuffix(i)
. Sincesuffix(i)
is in our compressed trie (i.e., suffix tree), we can search forP
by using the strategy to search for a key prefix in a compressed trie.
Let's search for the patternP = per
in the stringS = peeper
. Imagine that we have already constructed the suffix tree (Figure 4) forpeeper
. The search starts at the root nodeA
. SinceP[1] = p
, we follow the edge whose label begins with the digitp
. When following this edge, we compare the remaining digits of the edge label with successive digits ofP
. Since these remaining label digits agree with the pattern digits, we reach the branch nodeC
. In getting to nodeC
, we have used the first two digits of the pattern. The third digit of the pattern isr
, and so, from nodeC
we follow the edge whose label begins withr
. Since this edge has no additional digits in its label, no additional digit comparisons are done and we reach the information nodeI
. At this time, the digits in the pattern have been exhausted and we conclude that the pattern is in the string. Since an information node is reached, we conclude that the pattern is actually a suffix of the stringpeeper
. In the actual suffix tree representation (rather than in the humane drawing), the information nodeI
contains the index4
which tells us that the patternP = per
begins at digit4
ofpeeper
(i.e.,P = suffix(4)
). Further, we can conclude thatper
appears exactly once inpeeper
; the search for a pattern that appears more than once terminates at a branch node, not at an information node.
Now, let us search for the patternP = eeee
. Again, we start at the root. Since the first character of the pattern ise
, we follow the edge whose label begins withe
and reach the nodeB
. The next digit of the pattern is alsoe
, and so, from nodeB
we follow the edge whose label begins withe
. In following this edge, we must compare the remaining digitsper
of the edge label with the following digitsee
of the pattern. We find a mismatch when the first pair(p,e)
of digits are compared and we conclude that the pattern does not appear inpeeper
.
Suppose we are to search for the patternP = p
. From the root, we follow the edge whose label begins withp
. In following this edge, we compare the remaining digits (only the digite
remains) of the edge label with the following digits (there aren't any) of the pattern. Since the pattern is exhausted while following this edge, we conclude that the pattern is a prefix of all keys in the subtrie rooted at nodeC
. We can find all occurrences of the pattern by traversing the subtrie rooted atC
and visiting the information nodes in this subtrie. If we want the location of just one of the occurrences of the pattern, we can use the index stored in the first component of the branch nodeC
(see Figure 3). When a pattern exhausts while following the edge to nodeX
, we say that nodeX
has been reached; the search terminates at nodeX
.
When searching for the patternP = rope
, we use the first digitr
ofP
and reach the information nodeD
. Since the the pattern has not been exhausted, we must check the remaining digits of the pattern against those of the key inD
. This check reveals that the pattern is not a prefix of the key inD
, and so the pattern does not appear inpeeper
.
The last search we are going to do is for the patternP = pepe
. Starting at the root of Figure 4, we move over the edge whose label begins withp
and reach nodeC
. The next unexamined digit of the search pattern isp
. So, from nodeC
, we wish to follow the edge whose label begins withp
. Since no edge satisfies this requirement, we conclude thatpepe
does not appear in the stringpeeper
.
Other Nifty Things You Can Do with a Suffix Tree
Once we have set up the suffix tree for a stringS
, we can tell whether or notS
contains a patternP
inO(|P|)
time. This means that if we have a suffix tree for the text of Shakespeare's play ``Romeo and Juliet,'' we can determine whether or not the phrasewherefore art thou
appears in this play with lightning speed. In fact, the time taken will be that needed to compare up to18
(the length of the search pattern) letters/digits. The search time is independent of the length of the play.
Other interesting things you can do at lightning speed are:
- Find all occurrences of a pattern
P
. This is done by searching the suffix tree forP
. IfP
appears at least once, the search terminates successfully either at an information node or at a branch node. When the search terminates at an information node, the pattern occurs exactly once. When we terminate at a branch nodeX
, all places where the pattern occurs can be found by visiting the information nodes in the subtrie rooted atX
. This visiting can be done in time linear in the number of occurrences of the pattern if we
- (a)
- Link all of the information nodes in the suffix tree into a chain, the linking is done in lexicographic order of the represented suffixes (which also is the order in which the information nodes are encountered in a left to right scan of the information nodes). The information nodes of Figure 4 will be linked in the order
E, F, G, H, I, D
.- (b)
- In each branch node, keep a reference to the first and last information node in the subtrie of which that branch node is the root. In Figure 4, nodes
A, B,
andC
keep the pairs(E, D), (E, G),
and(H,I)
, respectively. We use the pair(firstInformationNode, lastInformationNode)
to traverse the information node chain starting atfirstInformationNode
and ending atlastInformationNode
. This traversal yields all occurrences of patterns that begin with the string spelled by the edge labels from the root to the branch node. Notice that when(firstInformationNode, lastInformationNode)
pairs are kept in branch nodes, we can eliminate the branch node field that keeps a reference to an information node in the subtrie (i.e., the fieldelement
).
- Find all strings that contain a pattern
P
. Suppose we have a collectionS1, S2, ... Sk
of strings and we wish to report all strings that contain a query patternP
. For example, the genome databank contains tens of thousands of strings, and when a researcher submits a query string, we are to report all databank strings that contain the query string. To answer queries of this type efficiently, we set up a compressed trie (we may call this a multiple string suffix tree) that contains the suffixes of the stringS1$S2$...$Sk#
, where$
and#
are two different digits that do not appear in any of the stringsS1, S2, ..., Sk
. In each node of the suffix tree, we keep a list of all stringsSi
that are the start point of a suffix represented by an information node in that subtrie.
- Find the longest substring of
S
that appears at leastm > 1
times. This query can be answered inO(|S|)
time in the following way:
- (a)
- Traverse the suffix tree labeling the branch nodes with the sum of the label lengths from the root and also with the number of information nodes in the subtrie.
- (b)
- Traverse the suffix tree visiting branch nodes with information node count
>= m
. Determine the visited branch node with longest label length.
Note that step (a) needs to be done only once. Following this, we can do step (b) for as many values ofm
as is desired. Also, note that whenm = 2
we can avoid determining the number of information nodes in subtries. In a compressed trie, every subtrie rooted at a branch node has at least two information nodes in it.
- Find the longest common substring of the strings
S
andT
. This can be done in timeO(|S| + |T|)
as below:
- (a)
- Construct a multiple string suffix tree for
S
andT
(i.e., the suffix tree forS$T#
).- (b)
- Traverse the suffix tree to identify the branch node for which the sum of the label lengths on the path from the root is maximum and whose subtrie has at least one information node that represents a suffix that begins in
S
and at least one information node that represents a suffix that begins inT
.
Notice that the related problem to find the longest common subsequence ofS
andT
is solved inO(|S| * |T|)
time using dynamic programming (see Exercise 15.22 of the text).
How to Build Your Very Own Suffix Tree
Three Observations
To aid in the construction of the suffix tree, we add alongestProperSuffix
field to each branch node. ThelongestProperSuffix
field of a branch node that represents the nonempty stringY
points to the branch node for the longest proper suffix ofY
(this suffix is obtained by removing the first digit fromY
). ThelongestProperSuffix
field of the root is not used.
Figure 5 shows the suffix tree of Figure 4 with longest proper suffix pointers (often, we refer to the longest proper suffix pointer as simply the suffix pointer) included. Longest proper suffix pointers are shown as red arrows. NodeC
represents the stringpe
. The longest proper suffixe
ofpe
is represented by nodeB
. Therefore, the (longest proper) suffix pointer ofC
points to nodeB
. The longest proper suffix of the stringe
represented by nodeB
is the empty string. Since the root node represents the empty string, the longest proper suffix pointer of nodeB
points to the root nodeA
.
Figure 5 Suffix tree of Figure 4 augmented with suffix pointers
Observation 1 If the suffix tree for any stringS
has a branch node that represents the stringY
, then the tree also has a branch node that represents the longest proper suffixZ
ofY
.
Proof LetP
be the branch node forY
. SinceP
is a branch node, there are at least2
different digitsx
andy
such thatS
has a suffix that begins withYx
and another that begins withYy
. Therefore,S
has a suffix that begins withZx
and another that begins withZy
. Consequently, the suffix tree forS
must have a branch node forZ
.
Observation 2 If the suffix tree for any stringS
has a branch node that represents the stringY
, then the tree also has a branch node for each of the suffixes ofY
.
Proof Follows from Observation 1.
Note that the suffix tree of Figure 5 has a branch node forpe
. Therefore, it also must have branch nodes for the suffixese
andepsilon
ofpe
.
The concepts of the last branch node and the last branch index are useful in describing the suffix tree construction algorithm. The last branch node for suffixsuffix(i)
is the parent of the information node that representssuffix(i)
. In Figure 5, the last branch nodes for suffixes1
through6
areC, B, B, C, B,
andA
, respectively. For any suffixsuffix(i)
, the last branch indexlastBranchIndex(i)
is the index of the digit on which a branch is made at the last branch node forsuffix(i)
. In Figure 5,lastBranchIndex(1) = 3
becausesuffix(1) = peeper
;suffix(1)
is represented by information nodeH
whose parent is nodeC
; the branch atC
is made using the third digit ofsuffix(1)
; and the third digit ofsuffix(1)
isS[3]
. You may verify thatlastBranchIndex[1:6] = [3, 3, 4, 6, 6, 6]
.
Observation 3 In the suffix tree for any stringS
,lastBranchIndex(i) <= lastBranchIndex(i+1)
,1 <= i < n
.
Proof Left as an exercise.
Get Out That Hammer and Saw, and Start Building
To build your very own suffix tree, you must start with your very own string. We shall use the stringR = ababbabbaabbabb
to illustrate the construction procedure. Since the last digitb
ofR
appears more than once, we append a new digit#
toR
and build the suffix tree forS = R# = ababbabbaabbabb#
. With ann = 16
digit stringS
, you can imagine that this is going to be a rather long example. However, when you are done with this example, you will know everything you ever wanted to know about suffix tree construction.
Construction Strategy
The suffix tree construction algorithm starts with a root node that represents the empty string. This node is a branch node. At any time during the suffix tree construction process, exactly one of the branch nodes of the suffix tree will be designated the active node. This is the node from where the process to insert the next suffix begins. LetactiveLength
be the length of the string represented by the active node. Initially, the root is the active node andactiveLength = 0
. Figure 6 shows the initial configuration, the active node is shown in green.
Figure 6 Initial configuration for suffix tree construction
As we proceed, we shall add branch and information nodes to our tree. Newly added branch nodes will be colored magenta, and newly added information nodes will be colored cyan. Suffix pointers will be shown in red.
Suffixes are inserted into the tree in the ordersuffix(1), suffix(2), ..., suffix(n)
. The insertion of the suffixes in this order is accomplished by scanning the stringS
from left to right. Lettree(i)
be the compressed trie for the suffixessuffix(1), ..., suffix(i)
, and letlastBranchIndex(j, i)
be the last branch index forsuffix(j)
intree(i)
. LetminDistance
be a lower bound on the distance (measured by number of digits) from the active node to the last branch index of the suffix that is to be inserted. Initially,minDistance = 0
aslastBranchIndex(1,1) = 1
. When insertingsuffix(i)
, it will be the case thatlastBranchIndex(i,i) >= i + activeLength + minDistance
.
To insertsuffix(i+1)
intotree(i)
, we must do the following:
- Determine
lastBranchIndex(i+1, i+1)
. To do this, we begin at the current active node. The firstactiveLength
number of digits of the new suffix (i.e., digitsS[i+1], S[i+2], ..., S[i + activeLength]
) will be known to agree with the string represented by the active node. So, to determinelastBranchIndex(i+1,i+1)
, we examinine digitsactiveLength + 1
,activeLength + 2
, ..., of the new suffix. These digits are used to follow a path throughtree(i)
beginning at the active node and terminating whenlastBranchIndex(i+1,i+1)
has been determined. Some efficiencies result from knowing thatlastBranchIndex(i+1,i+1) >= i + 1 + activeLength + minDistance
.- If
tree(i)
does not have a branch nodeX
which represents the stringS[i]...S[lastBranchIndex(i+1,i+1)-1]
, then create such a branch nodeX
.- Add an information node for
suffix(i+1)
. This information node is a child of the branch nodeX
, and the label on the edge fromX
to the new information node isS[lastBranchIndex(i+1, i+1)]...S[n]
.
Back to the Example
We begin by insertingsuffix(1)
into the treetree(0)
that is shown in Figure 6. The root is the active node,activeLength = minDistance = 0
. The first digit ofsuffix(1)
isS[1] = a
. No edge from the active node (i.e., the root) oftree(0)
has a label that begins witha
(in fact, at this time, the active node has no edge at all). Therefore,lastBranchIndex(1,1) = 1
. So, we create an information node and an edge whose label is the entire string. Figure 7 shows the result,tree(1)
. The root remains the active node, andactiveLength
andminDistance
are unchanged.
Figure 7 After the insertion of the suffix ababbabbaabbabb#
In our drawings, we shall show the labels on edges that go to information nodes using the notationi+
, wherei
gives the index, inS
, where the label starts and the+
tells us that the label goes to the end of the string. Therefore, in Figure 7, the edge label1+
denotes the stringS[1]...S[n]
. Figure 7 also shows the stringS
. The newly inserted suffix is shown in cyan.
To insert the next suffix,suffix(2)
, we again begin at the active node examining digitsactiveLength + 1 = 1
,activeLength + 2 = 2
, ..., of the new suffix. Since, digit 1 of the new suffix isS[2] = b
and since the active node has no edge whose label begins withS[2] = b
,lastBranchIndex(2,2) = 2
. Therefore, we create a new information node and an edge whose label is2+
. Figure 8 shows the resulting tree. Once again, the root remains the active node andactiveLength
andminDistance
are unchanged.
Figure 8 After the insertion of the suffix babbabbaabbabb#
Notice that the tree of Figure 8 is the compressed trietree(2)
forsuffix(1)
andsuffix(2)
.
The next suffix,suffix(3)
, begins atS[3] = a
. Since the active node oftree(2)
(i.e., the root) has an edge whose label begins witha
,lastBranchIndex(3,3) > 3
. To determinelastBranchIndex(3,3)
, we must see more digits ofsuffix(3)
. In particular, we need to see as many additional digits as are needed to distinguish betweensuffix(1)
andsuffix(3)
. We first compare the second digitS[4] = b
of the new suffix and the second digitS[2] = b
of the edge label1+
. SinceS[4] = S[2]
, we must do additional comparisons. The next comparison is between the third digitS[5] = b
of the new suffix and the third digitS[3] = a
of the edge label1+
. Since these digits are different,lastBranchIndex(3,3)
is determined to be5
. At this time, we updateminDistance
to have the value2
. Notice that, at this time, this is the max value possible forminDistance
becauselastBranchIndex(3,3) = 5 = 3 + activeLength + minDistance
.
To insert the new suffix,suffix(3)
, we split the edge oftree(2)
whose label is1+
into two. The first split edge has the label1,2
and the label for the second split edge is3+
. In between the two split edges, we place a branch node. Additionally, we introduce an information node for the newly inserted suffix. Figure 9 shows the treetree(3)
that results. The edge label1,2
is shown as the digitsS[1]S[2] = ab
.
Figure 9 After the insertion of the suffix abbabbaabbabb#
The compressed trietree(3)
is incomplete because we have yet to put in the longest proper suffix pointer for the newly created branch nodeD
. The longest suffix for this branch node isb
, but the branch node forb
does not exist. No need to panic, this branch node will be the next branch node created by us.
The next suffix to insert issuffix(4)
. This suffix is the longest proper suffix of the most recently inserted suffix,suffix(3)
. The insertion process for the new suffix begins by updating the active node by following the suffix pointer in the current active node. Since the root has no suffix pointer, the active node is not updated. Therefore,activeLength
is unchanged also. However, we must updateminDistance
to ensurelastBranchIndex(4,4) >= 4 + activeLength + minDistance
. It is easy to see thatlastBranchIndex(i,i) <= lastBranchIndex(i+1,i+1)
for alli < n
. Therefore,lastBranchIndex(i+1,i+1) >= lastBranchIndex(i,i) >= i + activeLength + minDistance
. To ensurelastBranchIndex(i+1,i+1) >= i + 1 + activeLength + minDistance
, we must reduceminDistance
by1
.
SinceminDistance = 1
, we start at the active node (which is still the root) and move forward following the path dictated byS[4]S[5]...
. We do not compare the firstminDistance
digits as we follow this path, because a match is assured until we get to the point where digitminDistance + 1
(i.e.,S[5]
) of the new suffix is to be compared. Since the active node edge label that begins withS[4] = b
is more than one digit long, we compareS[5]
and the second digitS[3] = a
of this edge's label. Since the two digits are different, the edge is split in the same way we split the edge with label1+
. The first split edge has the label2,2 = b
, and the label on the second split edge is3+
; in between the two split edges, we place a new branch nodeF
, a new information nodeG
is created for the newly inserted suffix, this information node is connected to the branch nodeF
by an edge whose label is5+
. Figure 10 shows the resulting structure.
Figure 10 After the insertion of the suffix bbabbaabbabb#
We can now set the suffix pointer from the branch nodeD
that was created whensuffix(3)
was inserted. This suffix pointer has to go to the newly created branch nodeF
.
The longest proper suffix of the stringb
represented by nodeF
is the empty string. So, the suffix pointer in nodeF
is to point to the root node. Figure 11 shows the compressed trie with suffix pointers added. This trie istree(4)
.
Figure 11 Trie of Figure 10 with suffix pointers added
The construction of the suffix tree continues with an attempt to insert the next suffixsuffix(5)
. Sincesuffix(5)
is the longest proper suffix of the most recently inserted suffixsuffix(4)
, we begin by following the suffix pointer in the active node. However, the active node is presently the root and it has no suffix pointer. So, the active node is unchanged. To preserve the desired relationship amonglastBranchIndex, activeLength, minDistance
, and the index (5
) of the next suffix that is to be inserted, we must reduceminDistance
by one. So,minDistance
becomes zero.
SinceactiveLength = 0
, we need to examine digits ofsuffix(5)
beginning with the first oneS[5]
. The active node has an edge whose label begins withS[5] = b
. We follow the edge with labelb
comparing suffix digits and label digits. Since all digits agree, we reach nodeF
. NodeF
becomes the active node (whenever we encounter a branch node during suffix digit examination, the active node is updated to this encountered branch node) andactiveLength = 1
. We continue the comparison of suffix digits using an edge from the current active node. Since the next suffix digit to compare isS[6] = a
, we use an active node edge whose label begins witha
(in case such an edge does not exist,lastBranchIndex
for the new suffix isactiveLength + 1
). This edge has the label3+
. The digit comparisons terminate inside this label when digitS[10] = a
of the new suffix is compared with digitS[7] = b
of the edge label3+
. Therefore,lastBranchIndex(5,5) = 10
.minDistance
is set to its max possible value, which islastBranchIndex(5,5) - (index of suffix to be inserted) - activeLength = 10 - 5 - 1 = 4
.
To insertsuffix(5)
, we split the edge(F,C)
that is between nodesF
andC
. The split takes place at digitsplitDigit = 5
of the label of edge(F,C)
. Figure 12 shows the resulting tree.
Figure 12 After the insertion of the suffix babbaabbabb#
Next, we insertsuffix(6)
. Since this suffix is the longest proper suffix of the last suffixsuffix(5)
that we inserted, we begin by following the suffix link in the active node. This gets us to the tree root, which becomes the new active node.activeLength
becomes0
. Notice that when we follow a suffix pointer,activeLength
reduces by1
; the value ofminDistance
does not change becauselastBranchIndex(6,6) >= lastBranchIndex(5,5)
. Therefore, we still have the desired relationshiplastBranchIndex(6,6) >= 6 + activeLength + minDistance
.
From the new active node, we follow the edge whose label begins witha
. When an edge is followed, we do not compare suffix and label digits. SinceminDistance = 4
, we are assured that the first mismatch will occur five or more digits from here. Since the labelab
that begins witha
is2
digits long, we skip overS[6]
andS[7]
of the suffix, move to nodeD
, makeD
the active node, updateactiveLength
to be2
andminDistance
to be2
, and examine the label on the active node edge that begins withS[8] = b
. The label on this edge is5+
. We omit the comparisons with the first two digits of this label becauseminDistance = 2
and immediately compare the fifth digitS[10] = a
ofsuffix(6)
with the third digitS[7] = b
of the edge label. Since these are different, the edge is split at its third digit. The new branch node that results from the edge split is the node that the suffix pointer of nodeH
of Figure 12 is to point to. Figure 13 shows the tree that results.
Figure 13 After the insertion of the suffix abbaabbabb#
Notice that following the last insertion, the active node isD
,activeLength = 2
, andminDistance = 2
.
Next, we insertsuffix(7)
. Since this suffix is the longest proper suffix of the suffix just inserted, we can use a short cut to do the insertion. The short cut is to follow the suffix pointer in the current active nodeD
. By following this short cut, we skip over a number of digits that is1
less thanactiveLength
. In our example, we skip over2 - 1 = 1
digit ofsuffix(7)
. The short cut guarantees a match between the skipped over digits and the string represented by the node that is moved to. NodeF
becomes the new active node andactiveLength
is reduced by 1. Once again,minDistance
is unchanged. (You may verify that whenever a short cut is taken, leavingminDistance
unchanged satisfies the desired relationship amonglastBranchIndex, activeLength, minDistance
, and the index of the next suffix that is to be inserted.)
To insertsuffix(7)
, we useS[8] = b
(recall that because of the short cut we have taken to nodeF
, we must skip overactiveLength = 1
digit of the suffix) to determine the edge whose label is to be examined. This gets us the label5+
. Again, sinceminDistance = 2
, we are assured that digitsS[8]
andS[9]
of the suffix match with the first two digits of the edge label5+
. Since there is a mismatch at the third digit of the edge label, the edge is split at the third digit of its label. The suffix pointer of nodeJ
is to point to the branch node that is placed between the two parts of the edge just split. Figure 14 shows the result.
Figure 14 After the insertion of the suffix bbaabbabb#
Notice that following the last insertion, the active node isF
,activeLength = 1
, andminDistance = 2
. IflastBranchIndex(7,7)
had turned out to be greater than 10, we would increaseminDistance
tolastBranchIndex(7,7) - 7 - activeLength
.
To insertsuffix(8)
, we first take the short cut from the current active nodeF
to the root. The root becomes the new active node,activeLength
is reduced by1
andminDistance
is unchanged. We start the insert process at the new active node. SinceminDistance = 2
, we have to move at least3
digits down from the active node. The active node edge whose label begins withS[8] = b
has the labelb
. SinceminDistance = 2
, we must follow edge labels until we have skipped2
digits. Consequently, we move to nodeF
. NodeF
becomes the active node,minDistance
is reduced by the length1
of the label on the edge(A,F)
and becomes1
,activeLength
is increased by the length of the label on the edge(A,F)
and becomes1
, and we follow the edge(F,H)
whose label begins withS[9] = a
. This edge is to be split at the second digit of its edge label. The suffix pointer ofL
is to point to the branch node that will be inserted between the two edges created when edge(F,H)
is split. Figure 15 shows the result.
Figure 15 After the insertion of the suffix baabbabb#
The next suffix to insert issuffix(9)
. From the active nodeF
, we follow the suffix pointer to nodeA
, which becomes the new active node.activeLength
is reduced by1
to zero, andminDistance
is unchanged at1
. The active node edge whose label begins withS[9] = a
has the labelab
. SinceminDistance = 1
, we compare the second digit ofsuffix(9)
and the second digit of the edge label. Since these two digits are different, the edge(A,D)
is split at the second digit of its label. Further the suffix pointer of the branch nodeM
that was created when the last suffix was inserted into the trie, is to point to the branch node that will be placed between nodesA
andD
. Finally, since the newly created branch node represents a string whose length is one, its suffix pointer is to point to the root. Figure 16 shows the result.
Figure 16 After the insertion of the suffix aabbabb#
As you can see, creating a suffix trie can be quite tiring. Let's continue though, we have, so far, inserted only the first9
suffixes into our suffix tree.
For the next suffix,suffix(10)
, we begin with the rootA
as the active node. We would normally follow the suffix pointer in the active node to get to the new active node from which the insert process is to start. However, the root has no suffix pointer. Instead, we reduceminDistance
by one. The new value ofminDistance
is zero.
The insertion process begins by examining the active node edge (if any) whose label begins with the first digitS[10] = a
ofsuffix(10)
. Since the active node has an edge whose label begins witha
, additional digits are examined to determinelastBranchIndex(10,10)
. We follow a search path from the active node. This path is determined by the digits ofsuffix(10)
. Following this path, we reach nodeJ
. By examining the label on the edge(J,E)
, we determine thatlastBranchIndex(10,10) = 16
. NodeJ
becomes the active node,activeLength = 4
, andminDistance = 2
.
Whensuffix[10]
is inserted, the edge(J,E)
splits. The split is at the third digit of this edge's label. Figure 17 shows the tree after the new suffix is inserted.
Figure 17 After the insertion of the suffix abbabb#
To insert the next suffix,suffix(11)
, we first take a short cut by following the suffix pointer at the active node. This pointer gets us to nodeL
, which becomes the new active node. At this time,activeLength
is reduced by one and becomes3
. Next, we need to move forward fromL
by a number of digits greater thanminDistance = 2
. Since digitactiveLength + 1
ofsuffix(11)
isS[14] = b
we follow theb
edge ofL
. We omit comparing the firstminDistance
digits of this edge's label. The first comparison made is betweenS[16] = #
(digit of suffix) andS[7 + 2] = a
(digit of edge label). Since these two digits are different, edge(L,G)
is to be split. Splitting this edge (at its third digit) and setting the suffix pointer from the most recently created branch nodeR
gives us the tree of Figure 18.
Figure 18 After the insertion of the suffix bbabb#
To insert the next suffix,suffix(12)
, we first take the short cut from the current active nodeL
to the nodeN
. NodeN
becomes the new active node, and we begin comparingminDistance + 1 = 3
digits down from nodeN
. Edge(N,H)
is split. Figure 19 shows the tree after this edge has been split and after the suffix pointer from the most recently created branch nodeT
has been set.
Figure 19 After the insertion of the suffix babb#
When insertingsuffix(13)
, we follow the short cut from the active nodeN
to the branch nodeP
. NodeP
becomes the active node and we are to move down the tree by at leastminDistance + 1 = 3
digits. The active node edge whose label begins withS[14] = b
is used first. We reach nodeD
, which becomes the active node, andminDistance
becomes1
. At nodeD
, we use the edge whose label begins withS[15] = b
. Since the label on this edge is two digits long, and since the second digit of this label differs fromS[16]
, this edge is to split. Figure 20 shows the tree after the edge is split and the suffix pointer from nodeV
is set.
Figure 20 After the insertion of the suffix abb#
To insertsuffix(14)
, we take the short cut from the current active nodeD
to the branch nodeF
. NodeF
becomes the active node. From nodeF
, we must move down by at leastminDistance + 1 = 2
digits. We use the edge whose label begins withS[15] = b
(S[15]
is used because it isactiveLength = 1
digits from the start ofsuffix(14)
). The split takes place at the second digit of edge(F,L)
's label. Figure 21 shows the new tree.
Figure 21 After the insertion of the suffix bb#
The next suffix to insert begins atS[15] = b
. We take the short cut from the current active nodeF
, to the root. The root is made the current active node and then we move down byminDistance + 1 = 2
digits. We follow the active node edge whose label begins withb
and reach nodeF
. A new information node is added toF
. The suffix pointer for the last branch nodeZ
is set to point to the current active nodeF
, and the root becomes the new active node. Figure 22 shows the new tree.
Figure 22 After the insertion of the suffix b#
Don't despair, only one suffix remains. Since no suffix is a proper prefix of another suffix, we are assured that the root has no edge whose label begins with the last digit of the stringS
. We simply insert an information node as a child of the root. The label for the edge to this new information node is the last digit of the string. Figure 23 shows the complete suffix tree for the stringS = ababbabbaabbabb#
. The suffix pointers are not shown as they are no longer needed; the space occupied by these pointers may be freed.
Figure 23 Suffix tree for ababbabbaabbabb#
Complexity Analysis
Letr
denote the number of different digits in the stringS
whose suffix tree is to be built (r
is the alphabet size), and letn
be the number of digits (and hence the number of suffixes) of the stringS
.
To insertsuffix(i)
, weThe total time spent in part (a) (over all
- (a)
- Follow a suffix pointer in the active node (unless the active node is the root).
- (b)
- Then move down the existing suffix tree until
minDistance
digits have been crossed.- (c)
- Then compare some number of suffix digits with edge label digits until
lastBranchIndex(i,i)
is determined.- (d)
- Finally insert a new information node and possibly also a branch node.
n
inserts) isO(n)
.
When moving down the suffix tree in part (b), no digit comparisons are made. Each move to a branch node at the next level takesO(1)
time. Also, each such move reduces the value ofminDistance
by at least one. SinceminDistance
is zero initially and never becomes less than zero, the total time spent in part (b) isO(n + total amount by which minDistance is increased over all n inserts)
.
In part (c),O(1)
time is spent determining whetherlastBranchIndex(i,i) = i + activeLength + minDistance
. This is the case iffminDistance = 0
or the digitx
at positionactiveLength + minDistance + 1
ofsuffix(i)
is not the same as the digit in positionminDistance + 1
of the label on the appropriate edge of the active node. WhenlastBranchIndex(i,i) != i + activeLength + minDistance
,lastBranchIndex(i,i) > i + activeLength + minDistance
and the value oflastBranchIndex(i,i)
is determined by making a sequence of comparisons between suffix digits and edge label digits (possibly involving moves downwards to new branch nodes). For each such comparison that is made,minDistance
is increased by1
. This is the only circumstance under whichminDistance
increases in the algorithm. So, the total time spent in part (c) isO(n + total amount by which minDistance is increased over all n inserts)
. Since each unit increase in the value ofminDistance
is the result of an equal compare between a digit at a new position (i.e., a position from which such a compare has not been made previously) of the stringS
and an edge label digit, the total amount by whichminDistance
is increased over alln
inserts isO(n)
.
Part (d) takesO(r)
time per insert, because we need to initialize theO(r)
fields of the branch node that may be created. The total time for part (d) is, therefore,O(nr)
.
So, the total time taken to build the suffix tree isO(nr)
. Under the assumption that the alphabet sizer
is constant, the complexity of our suffix tree generation algorithm becomesO(n)
.
The use of branch nodes with as many children fields as the alphabet size is recommended only when the alphabet size is small. When the alphabet size is large (and it may be as large asn
, making the above algorithm anO(n2)
algorithm), the use of a hash table results in an expected time complexity ofO(n)
. The space complexity changes fromO(nr)
toO(n)
.
A divide-and-conquer algorithm that has a time and space complexity ofO(n)
(even when the alphabet size isO(n)
) is developed in Optimal suffix tree construction with large alphabets.
Exercises
- Draw the suffix tree for
S = ababab#
.- Draw the suffix tree for
S = aaaaaa#
.- Draw the multiple string suffix tree for
S1 = abba, S2 = bbbb,
ands3 = aaaa
.- Prove Observation 3.
- Draw the trees
tree(i), 1 < = i < = |S|
forS = bbbbaaaabbabbaa#
. Show the active node in each tree. Also, show the longest proper suffix pointers.- Draw the trees
tree(i), 1 < = i < = |S|
forS = aaaaaaaaaaaa#
. Show the active node in each tree. Also, show the longest proper suffix pointers.- Develop the class
SuffixTree
. Your class should include a method to create the suffix tree for a given string as well as a method to search a suffix tree for a given pattern. Test the correctness of your methods.- Explain how you can obtain the multiple string suffix tree for
S1, ..., Sk
from that forS1, ..., S(k-1)
. What is the time complexity of your proposed method?
References and Selected Readings
You can learn more about the genome project and genomic applications of pattern matching from the following Web sites:
- NIH's Web site for the human genome project
- Department of Energy's Web site for the human genomics project
- Biocomputing Hypertext Coursebook.
Linear time algorithms to search for a single pattern in a given string can be found in most algorithm's texts. See, for example, the texts:
- Computer Algorithms, by E. Horowitz, S. Sahni, and S. Rajasekeran, Computer Science Press, New York, 1998.
- Introduction to Algorithms, by T. Cormen, C. Leiserson, and R. Rivest, McGraw-Hill Book Company, New York, 1992.
For more on suffix tree construction, see the papers:
- ``A space economical suffix tree construction algorithm,'' by E. McCreight, Journal of the ACM, 23, 2, 1976, 262-272.
- ``On-line construction of suffix trees,'' by E. Ukkonen, Algorithmica, 14, 3, 1995, 249-260.
- Fast string searching with suffix trees,'' by M. Nelson, Dr. Dobb's Journal, August 1996.
- Optimal suffix tree construction with large alphabets, by M. Farach, IEEE Symposium on the Foundations of Computer Science, 1997.
You can download C++ code to construct a suffix tree from http://www.ddj.com/ftp/1996/1996.08/suffix.zip. This code, developed by M. Nelson, is described in paper 3 above.