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 stringSand a patternPand are to report whether or not the patternPoccurs in the stringS. For example, the patternP = catappears (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 around2000letters 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 patternPin 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 patternPcould 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, ..., Pkin 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 forShas been built). Therefore once the suffix tree forShas been created, the time needed to search for a pattern depends only on the length of the pattern.
The Suffix Tree
The suffix tree forSis 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 = peeperarepeeper, eeper, eper, per, er,andr. Therefore, the suffix tree for the stringpepperis the compressed trie that contains the elements (which are also the keys)peeper, eeper, eper, per, er,andr. The alphabet for the stringpeeperis{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-Iare the suffixes ofpeeper, each information node need retain only the start index of the suffix it contains. When the letters inpeeperare indexed from left to right beginning with the index1, the information nodesD-Ineed 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. NodeAof Figure 4 represents the empty stringepsilon, nodeCrepresents the stringpe, and nodeFrepresents 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 stringSappears only once inS, no suffix ofScan be a proper prefix of another suffix ofS. In the stringpeeper, the last digit isr, and this digit appears only once. Therefore, no suffix ofpeeperis a proper prefix of another. The last digit ofdataisa, and this last digit appears twice indata. Therefore,datahas two suffixesataandathat begin witha. The suffixais a proper prefix of the suffixata.
When the last digit of the stringSappears more than once inSwe must append a new digit (say#) to the suffixes ofSso that no suffix is a prefix of another. Optionally, we may append the new digit toSto 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 ofSfrom left to right beginning with the number1.S[i]denotes theith 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 patternPin a stringSis thatPappears inS(i.e.,Pis a substring ofS) iffPis a prefix of some suffix ofS.
Suppose thatP = P[1]...P[k] = S[i]...S[i+k-1]. Then,Pis a prefix ofsuffix(i). Sincesuffix(i)is in our compressed trie (i.e., suffix tree), we can search forPby using the strategy to search for a key prefix in a compressed trie.
Let's search for the patternP = perin 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 nodeCwe 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 nodeIcontains the index4which tells us that the patternP = perbegins at digit4ofpeeper(i.e.,P = suffix(4)). Further, we can conclude thatperappears 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 witheand reach the nodeB. The next digit of the pattern is alsoe, and so, from nodeBwe follow the edge whose label begins withe. In following this edge, we must compare the remaining digitsperof the edge label with the following digitseeof 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 digiteremains) 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 atCand 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 nodeXhas been reached; the search terminates at nodeX.
When searching for the patternP = rope, we use the first digitrofPand 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 withpand 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 thatpepedoes 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 notScontains a patternPinO(|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 thouappears 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. IfPappears 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,andCkeep the pairs(E, D), (E, G),and(H,I), respectively. We use the pair(firstInformationNode, lastInformationNode)to traverse the information node chain starting atfirstInformationNodeand 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, ... Skof 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 stringsSithat are the start point of a suffix represented by an information node in that subtrie.
- Find the longest substring of
Sthat appears at leastm > 1times. 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 ofmas is desired. Also, note that whenm = 2we 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
SandT. This can be done in timeO(|S| + |T|)as below:
- (a)
- Construct a multiple string suffix tree for
SandT(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
Sand at least one information node that represents a suffix that begins inT.
Notice that the related problem to find the longest common subsequence ofSandTis 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 alongestProperSuffixfield to each branch node. ThelongestProperSuffixfield of a branch node that represents the nonempty stringYpoints to the branch node for the longest proper suffix ofY(this suffix is obtained by removing the first digit fromY). ThelongestProperSuffixfield 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. NodeCrepresents the stringpe. The longest proper suffixeofpeis represented by nodeB. Therefore, the (longest proper) suffix pointer ofCpoints to nodeB. The longest proper suffix of the stringerepresented by nodeBis the empty string. Since the root node represents the empty string, the longest proper suffix pointer of nodeBpoints to the root nodeA.![]()
Figure 5 Suffix tree of Figure 4 augmented with suffix pointers
Observation 1 If the suffix tree for any stringShas a branch node that represents the stringY, then the tree also has a branch node that represents the longest proper suffixZofY.
Proof LetPbe the branch node forY. SincePis a branch node, there are at least2different digitsxandysuch thatShas a suffix that begins withYxand another that begins withYy. Therefore,Shas a suffix that begins withZxand another that begins withZy. Consequently, the suffix tree forSmust have a branch node forZ.
Observation 2 If the suffix tree for any stringShas 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 suffixeseandepsilonofpe.
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 suffixes1through6areC, 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) = 3becausesuffix(1) = peeper;suffix(1)is represented by information nodeHwhose parent is nodeC; the branch atCis 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 = ababbabbaabbabbto illustrate the construction procedure. Since the last digitbofRappears more than once, we append a new digit#toRand build the suffix tree forS = R# = ababbabbaabbabb#. With ann = 16digit 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. LetactiveLengthbe 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 stringSfrom 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). LetminDistancebe 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 = 0aslastBranchIndex(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 firstactiveLengthnumber 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 nodeXwhich 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 fromXto 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, andactiveLengthandminDistanceare 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+, whereigives 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] = band 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 andactiveLengthandminDistanceare 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] = bof the new suffix and the second digitS[2] = bof the edge label1+. SinceS[4] = S[2], we must do additional comparisons. The next comparison is between the third digitS[5] = bof the new suffix and the third digitS[3] = aof the edge label1+. Since these digits are different,lastBranchIndex(3,3)is determined to be5. At this time, we updateminDistanceto have the value2. Notice that, at this time, this is the max value possible forminDistancebecauselastBranchIndex(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,2and 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,2is 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 forbdoes 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,activeLengthis unchanged also. However, we must updateminDistanceto 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 reduceminDistanceby1.
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 firstminDistancedigits 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] = bis more than one digit long, we compareS[5]and the second digitS[3] = aof 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 nodeGis created for the newly inserted suffix, this information node is connected to the branch nodeFby 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 nodeDthat was created whensuffix(3)was inserted. This suffix pointer has to go to the newly created branch nodeF.
The longest proper suffix of the stringbrepresented by nodeFis the empty string. So, the suffix pointer in nodeFis 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 reduceminDistanceby one. So,minDistancebecomes 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 labelbcomparing suffix digits and label digits. Since all digits agree, we reach nodeF. NodeFbecomes 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,lastBranchIndexfor the new suffix isactiveLength + 1). This edge has the label3+. The digit comparisons terminate inside this label when digitS[10] = aof the new suffix is compared with digitS[7] = bof the edge label3+. Therefore,lastBranchIndex(5,5) = 10.minDistanceis 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 nodesFandC. The split takes place at digitsplitDigit = 5of 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.activeLengthbecomes0. Notice that when we follow a suffix pointer,activeLengthreduces by1; the value ofminDistancedoes 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 labelabthat begins withais2digits long, we skip overS[6]andS[7]of the suffix, move to nodeD, makeDthe active node, updateactiveLengthto be2andminDistanceto 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 = 2and immediately compare the fifth digitS[10] = aofsuffix(6)with the third digitS[7] = bof 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 nodeHof 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 is1less thanactiveLength. In our example, we skip over2 - 1 = 1digit ofsuffix(7). The short cut guarantees a match between the skipped over digits and the string represented by the node that is moved to. NodeFbecomes the new active node andactiveLengthis reduced by 1. Once again,minDistanceis unchanged. (You may verify that whenever a short cut is taken, leavingminDistanceunchanged 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 = 1digit 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 nodeJis 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 increaseminDistancetolastBranchIndex(7,7) - 7 - activeLength.
To insertsuffix(8), we first take the short cut from the current active nodeFto the root. The root becomes the new active node,activeLengthis reduced by1andminDistanceis unchanged. We start the insert process at the new active node. SinceminDistance = 2, we have to move at least3digits down from the active node. The active node edge whose label begins withS[8] = bhas the labelb. SinceminDistance = 2, we must follow edge labels until we have skipped2digits. Consequently, we move to nodeF. NodeFbecomes the active node,minDistanceis reduced by the length1of the label on the edge(A,F)and becomes1,activeLengthis 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 ofLis 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.activeLengthis reduced by1to zero, andminDistanceis unchanged at1. The active node edge whose label begins withS[9] = ahas 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 nodeMthat was created when the last suffix was inserted into the trie, is to point to the branch node that will be placed between nodesAandD. 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 first9suffixes into our suffix tree.
For the next suffix,suffix(10), we begin with the rootAas 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 reduceminDistanceby one. The new value ofminDistanceis zero.
The insertion process begins by examining the active node edge (if any) whose label begins with the first digitS[10] = aofsuffix(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. NodeJbecomes 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,activeLengthis reduced by one and becomes3. Next, we need to move forward fromLby a number of digits greater thanminDistance = 2. Since digitactiveLength + 1ofsuffix(11)isS[14] = bwe follow thebedge ofL. We omit comparing the firstminDistancedigits 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 nodeRgives 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 nodeLto the nodeN. NodeNbecomes the new active node, and we begin comparingminDistance + 1 = 3digits 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 nodeThas been set.![]()
Figure 19 After the insertion of the suffix babb#
When insertingsuffix(13), we follow the short cut from the active nodeNto the branch nodeP. NodePbecomes the active node and we are to move down the tree by at leastminDistance + 1 = 3digits. The active node edge whose label begins withS[14] = bis used first. We reach nodeD, which becomes the active node, andminDistancebecomes1. 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 nodeVis set.![]()
Figure 20 After the insertion of the suffix abb#
To insertsuffix(14), we take the short cut from the current active nodeDto the branch nodeF. NodeFbecomes the active node. From nodeF, we must move down by at leastminDistance + 1 = 2digits. We use the edge whose label begins withS[15] = b(S[15]is used because it isactiveLength = 1digits 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 = 2digits. We follow the active node edge whose label begins withband reach nodeF. A new information node is added toF. The suffix pointer for the last branch nodeZis 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
Letrdenote the number of different digits in the stringSwhose suffix tree is to be built (ris the alphabet size), and letnbe 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
minDistancedigits 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.
ninserts) 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 ofminDistanceby at least one. SinceminDistanceis 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 = 0or the digitxat positionactiveLength + minDistance + 1ofsuffix(i)is not the same as the digit in positionminDistance + 1of the label on the appropriate edge of the active node. WhenlastBranchIndex(i,i) != i + activeLength + minDistance,lastBranchIndex(i,i) > i + activeLength + minDistanceand 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,minDistanceis increased by1. This is the only circumstance under whichminDistanceincreases 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 ofminDistanceis 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 stringSand an edge label digit, the total amount by whichminDistanceis increased over allninserts 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 sizeris 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, ..., Skfrom 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.