<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Acdhirr's Tech Talk]]></title><description><![CDATA[Here I write about everything I experience or make up, insofar as it relates to computer programming.]]></description><link>https://acdhirr.hashnode.dev</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1593680282896/kNC7E8IR4.png</url><title>Acdhirr&apos;s Tech Talk</title><link>https://acdhirr.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Sat, 05 Sep 2026 20:08:14 GMT</lastBuildDate><atom:link href="https://acdhirr.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[A strategy for constructing a suffix tree quickly and efficiently with minimal memory usage]]></title><description><![CDATA[A suffix tree is a data structure designed for instant indexing and searching of large texts and character strings, such as documents or DNA sequences. It constructs a compressed tree of every possibl]]></description><link>https://acdhirr.hashnode.dev/a-strategy-for-constructing-a-suffix-tree-quickly-and-efficiently-with-minimal-memory-usage</link><guid isPermaLink="true">https://acdhirr.hashnode.dev/a-strategy-for-constructing-a-suffix-tree-quickly-and-efficiently-with-minimal-memory-usage</guid><category><![CDATA[algorithms]]></category><category><![CDATA[SuffixTree ]]></category><category><![CDATA[Computer Science]]></category><category><![CDATA[Data Science]]></category><category><![CDATA[Scala]]></category><dc:creator><![CDATA[Richard Osseweyer]]></dc:creator><pubDate>Wed, 10 Jun 2026 08:30:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a20824202f7c5ee1fada4ba/af166d76-ccdf-4c8d-829b-907f20cab1a5.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>A suffix tree is a data structure designed for instant indexing and searching of large texts and character strings, such as documents or DNA sequences. It constructs a compressed tree of every possible text ending, enabling computers to rapidly search for patterns in these large datasets.</p>
<p>One drawback of the suffix tree is that it demands much more memory than the text it represents and the same applies to its construction. Its time complexity can quickly become unmanageable.</p>
<p>For an online CS course I was asked to build a suffix tree. The algorithm I initially submitted did not meet the assessment system’s requirements in terms of speed and memory usage and, as a result, I had to start all over.</p>
<p>As it turned out, when building a suffix tree, one must use memory and processing power as efficiently as possible.</p>
<p>What I will show here is a strategy to do exactly that, with a smarter but still relatively simple algorithm that nevertheless offers significant gains in speed and memory usage compared to the naive method. This is not Ukkonen's algorithm, because it still operates in O(n²) time, but for its class it still is quite fast.</p>
<h2>Creating a suffix tree in three stages</h2>
<p>Let's start with a description of the algorithm I initially built, as it perfectly and clearly illustrates what needs to be done. It follows a commonly used and logical approach.</p>
<p>We start with a suffix <em>trie</em>. A trie is a specialized tree for storing single characters. Gradually the suffix <em>trie</em> is compressed into a suffix <em>tree</em>.</p>
<p>The tree and trie (from re<em>trie</em>val) datastructures start with a single root node, which more or less corresponds to the trunk of a real tree, and which has branches with perhaps further branches and eventually leaves.</p>
<div>
<div>👉</div>
<div>Note that <em>tree</em> and <em>trie</em> are closely related but still different concepts. Though derived from the word 'retrieval', <em>trie</em> is pronounced as 'try' [/traɪ/] to signal the difference.</div>
</div>

<p>Creating a suffix <em>tree</em> for a text <em>T</em> can relatively easy be done by first building a suffix <em>trie</em> character by character out of all suffixes of a string, then compressing all single child nodes into multi-character nodes, and finally replacing the labels on those nodes by their coordinates in the text. This is the three-step approach.</p>
<p>It might not be immediately clear what this is all about, so let's look at it step by step. To start with, consider the suffixes for <em>T</em> = 'banana' (using $ as a unique closing character):</p>
<pre><code class="language-java">0 BANANA$
1  ANANA$
2   NANA$
3    ANA$
4     NA$
5      A$
6       $
</code></pre>
<p>There are 7 suffixes that we want to store in a tree. Let's first build the suffix trie for 'banana' by adding the characters of the suffix strings above top to bottom to a root node. When, starting from root, a character is already on the trie, do not add it, but move on to the next character and branch off as soon as you reach a character that is not yet on the branch. Doing so, we save space by reusing common parts of the strings.</p>
<p>As an example, adding the first three suffixes (0, 1 and 2 in the list above) results in this intermediate state of the trie:</p>
<pre><code class="language-java">         |---B---A---N---A---N---A---$  (0)
         |       
ROOT-----|---A---N---A---N---A---$  (1)
         |
         |---N---A---N---A---$  (2)
</code></pre>
<p>These first three suffixes do not share any prefix characters, so they all branch off immediately from the root. However, when inserting the fourth suffix ('ANA$') we encounter a shared prefix on the 2nd branch in the picture above. It's the first A on the 2nd branch, and the subsequent N and A are also shared.</p>
<p>We can reuse the first 3 characters on the '<strong>ANA</strong>NA\(' branch until we reach the second 'N'. The 'N' character obviously does not match the '\)' character in our suffix 'ANA$', so there we split the branch:</p>
<pre><code class="language-java">         |---B---A---N---A---N---A---$  (0)
         |       
         |               |---N---A---$  (1)
ROOT-----|---A---N---A---|
         |               |---$  (3)
         |
         |---N---A---N---A---$  (2)
</code></pre>
<p>Now there are four suffixes on the trie, representing 7 + 6 + 5 + 4 = 22 characters, but there are only 19 characters on the trie, so we saved 3 characters. Continuing likewise with the remaining suffixes we get the complete suffix trie for 'banana':</p>
<pre><code class="language-java">         |---B---A---N---A---N---A---$  (0)
         |
         |                   |---N---A---$  (1)
         |       |---N---A---|
         |       |           |---$  (3)
         |---A---|
         |       |
ROOT-----|       |---$  (5)
         |
         |           |---N---A---$  (2)
         |---N---A---|
         |           |---$  (4)
         |
         |---$  (6)
</code></pre>
<p>Though all the suffixes together count 28 characters, the trie contains only 22. We did save some space.</p>
<p>Unfortunately, for longer texts this structure still becomes rather bulky in terms of memory usage.</p>
<p>Can we save more space? Fortunately, it is indeed possible to save even more space by taking a few additional measures:</p>
<ol>
<li><p>Reassemble the non-branching runs of nodes (that all carry a single character) into a single node spelling out a multi-character substring of text <em>T</em>;</p>
</li>
<li><p>replace the resulting (potentially long) multi-character strings by their position and length in text <em>T</em>.</p>
</li>
</ol>
<p>Compressing the non-branching nodes into multi-character nodes (step 1) significantly reduces the number of nodes from 22 to 11. Now this is what is called a suffix <em>tree</em> (not <em>trie</em>):</p>
<pre><code class="language-java">         |----BANANA$  (0)
         |
         |                |---NA$  (1)
         |       |---NA---|
         |       |        |---$  (3)
         |---A---|
         |       |
ROOT-----|       |---$  (5)
         |
         |        |---NA$  (2)
         |---NA---|
         |        |---$  (4)
         |
         |---$  (6)
</code></pre>
<p>The amount of nodes is significantly smaller, but still the amount of characters in the tree remains the same as before and the substrings on the node labels are copied over and over.</p>
<p>Therefore, to further reduce memory usage we replace each label with a number pair describing the label's starting position in text <em>T</em> and its length (step 2):</p>
<pre><code class="language-java">
Text:     BANANA$
Position: 0123456

         |-----(0,7)  (0)
         |
         |                   |--(4,3)  (1)
         |         |--(2,2)--|
         |         |         |--(6,1)  (3)
         |--(1,1)--|
         |         |
ROOT-----|         |--(6,1)  (5)
         |
         |         |--(4,3)  (2)
         |--(2,2)--|
         |         |--(6,1)  (4)
         |
         |--(6,1)  (6)
</code></pre>
<p>Now this makes a difference, since longer substrings require no more memory than the shorter ones. The tree no longer contains copied text parts; instead, it entirely consists of pointers to the text <em>T</em>, which is kept in memory just once.</p>
<p>To summarise, the above procedure for building a compressed suffix tree consists of three steps:</p>
<ol>
<li><p>Create a suffix trie for text <em>T</em>;</p>
</li>
<li><p>compress each run of non-branching single character nodes into a single multi-character node;</p>
</li>
<li><p>replace the labels on the resulting multi-character nodes with their pointers in the text <em>T</em>.</p>
</li>
</ol>
<p>Steps 2 and 3 can be merged into a single process, but steps 1 and 2 require separate passes. Moreover, though compressed in the end, the structure still takes up lots of memory immediately after step 1.</p>
<p><strong>That begs the question: can <em>all 3</em> steps be carried out in a single loop, keeping memory usage to a minimum at every stage in the process? Can we build a compressed suffix <em>tree</em> directly without building an intermediate suffix <em>trie</em> first?</strong></p>
<h2>Building a compressed suffix tree with a smarter algorithm</h2>
<p>The answer is: yes, and it significantly reduces memory usage, thereby greatly speeding up the construction of the suffix tree.</p>
<p>However, the algorithm becomes slightly more complex compared to the three-step approach. You’ll have to trust me on that because I will skip showing the code for the three-step approach above and head over directly to the single pass algorithm.</p>
<p>Again we add the suffixes to the root, starting with the longest suffix and ending with the single character closing sign ('$'). This time however the nodes carry strings and not just single characters.</p>
<p>Let's add the first 3 suffixes to the root. Like before, each one of them branches off immediately from the root since they do not share a prefix:</p>
<pre><code class="language-java">         |---BANANA$  (0)
         |
ROOT-----|---ANANA$  (1)      
         |
         |---NANA$  (2)
</code></pre>
<p>Things change when adding the fourth suffix 'ANA\( to the tree'. Its first three characters are already on the second branch ('ANANA\)'). These two suffixes can thus share the common prefix 'ANA' and branch from where they differ:</p>
<pre><code class="language-java">         |---BANANA$  (0)
         |
         |         |---NA$  (1)
ROOT-----|---ANA---|
         |         |---$  (3)
         |
         |---NANA$  (2)
</code></pre>
<p>This time we had to split a node when branching. In the (single character) <em>trie</em> that would never occur. So this adds a bit of extra complexity: where and how do you split a node?</p>
<p>Continuing, we achieve the same tree as before, but without the compression step, since the nodes were already compressed, and split when necessary:</p>
<pre><code class="language-java">         |---BANANA$  (0)
         |
         |                |---NA$  (1)
         |       |---NA---|
         |       |        |---$  (3)
ROOT-----|---A---|
         |       |
         |       |---$ (5)
         |
         |
         |        |---NA$  (2)
         |---NA---|
         |        |---$  (4)
         |
         |---$  (6)
</code></pre>
<p>For illustration, the text representations are shown on the nodes above, but in the real algorithm, the strings are represented by their numeric pointer values right from the start. This prevents the tree from still taking up too much memory during construction.</p>
<h2>Constructing the algorithm</h2>
<p>Let's see if we can condense all the above thoughts into a pseudo-code algorithm and build a suffix tree in a single pass.</p>
<h3>Pseudo code algorithm</h3>
<p>For every suffix <code>S</code> of text <em>T</em>:</p>
<ol>
<li><p>set the current <code>node</code> to the root node;</p>
</li>
<li><p>see if one of the <code>node</code>'s children shares a prefix with suffix <code>S</code>;</p>
</li>
<li><p>if there is no common prefix, add a new node for suffix <code>S</code> to <code>node</code>. Stop here and move on to the next suffix;</p>
</li>
<li><p>but if suffix <code>S</code> shares a common prefix with one of the current <code>node</code>'s children, determine whether the child node <code>child</code> containing the prefix must be split in two:</p>
<ol>
<li><p>if the entire label of the <code>child</code> node <em>is</em> the prefix, do not split it;</p>
</li>
<li><p>if the label of the <code>child</code> node extends beyond the prefix, split the <code>child</code> node immediately after the prefix;</p>
</li>
</ol>
</li>
<li><p>repeat step 2 with the current node set to the <code>child</code> node identified in step 4, and remove the common prefix from the beginning of the suffix. Eventually each recursion will end at step 3.</p>
</li>
</ol>
<h3>Splitting a node on a common prefix</h3>
<p>As an aside, let's leave the suffixes for a moment and use a few common words for the sake of the example. We must distinguish two cases of commonality in prefixes:</p>
<ol>
<li><p>Strings 'disco' and 'disciple' share prefix 'disc'. The first string <em>shares a prefix with</em> the second.</p>
</li>
<li><p>Strings 'disc' and 'disclaimer' also share prefix 'disc'. In this case however the first string <em>is a prefix for</em> the second.</p>
</li>
</ol>
<p>This distinction dictates whether a node needs to be split.</p>
<p>Imagine a node 'DISC' is on a tree containing both 'DISCO' and 'DISCIPLE'. Adding 'DISCLAIMER' does not require splitting the 'DISC' node:</p>
<pre><code class="language-java">                |---O   
ROOT-----DISC---|
                |---IPLE

                ↑

                | 
                |---LAIMER
</code></pre>
<p>How about adding 'DISTANT'? It shares a prefix with 'DISC', but only part of it, so now we do have to split 'DISC' in two parts:</p>
<p>'DIS' (the prefix) and 'C' (the remainder):</p>
<pre><code class="language-java">                       |---O
                       |
               |---C---|---IPLE
               |       |
               |       |---LAIMER   
ROOT-----DIS---|

               ↑

               |
               |---TANT
               
</code></pre>
<p>The remainder 'C' keeps the 3 children of the previously undivided node, while the node itself is shortened to the common prefix 'DIS'. The remainder 'C' (including the tree below it) together with the suffix minus the common prefix ('<s>DIS </s> TANT') are inserted as the shortened node's new children.</p>
<h2>Writing the algorithm</h2>
<p>I use Scala to write the algorithm. Scala is concise and offers some expressiveness not found in Java, while still being strongly typed, which makes coding much easier in my opinion.</p>
<p>The <code>Node</code> class models a node in the tree. It has fields for <code>children</code> (a list), and <code>start</code> and <code>length</code> (integers), pointing to the text <em>T</em> which will be stored in global variable <code>allText</code>.</p>
<p>Note that contrary to the tree examples above, as said, no strings will be stored on the nodes. There will only be pointers to sections of <code>allText</code>. This certainly makes programming more complex, which is why, for debugging purposes, the <code>toString</code> function is overridden to convert the <code>(start,length)</code> pointers - that define the sections - back into actually readable text.</p>
<pre><code class="language-java">val allText = "..." 

class Node( var start: Int = 0, var length: Int = 0 ) {

  // An empty list to hold the child nodes
  var children = mutable.MutableList[Node]()

  override def toString: String =
    f"${allText.substring(start,start+length)}"
}
</code></pre>
<p>Building the tree consists of generating all suffixes from the text <code>allText</code> and adding them to the tree. We initialize the tree with an empty root node, then add the suffixes one by one.</p>
<div>
<div>👉</div>
<div>The steps in the pseudocode algorithm presented above are referenced in code comments in these code listings as [PC-X], where X represents the corresponding step number.</div>
</div>

<p>The first suffix added is the entire text <code>allText</code> (including the closing character '\('), and the last suffix to be added is just the closing character '\)'.</p>
<pre><code class="language-java">private def buildTree(): Node = {

  /* Initialise the empty root node
  */  
  val tree = new Node()

  /* Add suffixes 
  */  
  for (i &lt;- 0 until allText.length) 

    // [PC-1]
    addSuffixToNode(i, tree)  

  /* Return the resulting tree
  */  
  tree
}
</code></pre>
<h3>The main procedure</h3>
<p>The main procedure is <code>addSuffixToNode</code>, which updates the tree we have built so far by inserting a suffix. There are two helpers methods, <code>commonPrefix</code> and <code>splitNodeOnPrefix</code>, which will be explained later. Please follow along with the code comments to see what happens where. The <code>[PC-X]</code> codes refer to the pseudo algorithm shown previously.</p>
<p>Remember, none of these methods handles any String data. All strings are represented by their coordinates in <code>allText</code>.</p>
<pre><code class="language-java">@tailrec
private def addSuffixToNode(

  suffixStart: Int, 
  node: Node,
  searchPrefix: Boolean = true ): Unit = {

  /* 'sharedPrefix' will consist of a Node and the Int pair 
     pointing to the location of the prefix in allText
  */
  var sharedPrefix: Option[(Node, (Int, Int))] = None
  /*
     Boolean 'continue' allows us to bypass the commonPrefix
     call when we have found a common prefix or we can be sure 
     none will be found.
  */  
  var continue = searchPrefix 

  /* [PC-2]

     find a child node that shares a common prefix 
     with suffix 
  */
  if (continue) for (child &lt;- node.children) { 

    val (prefixStart, prefixLen) = commonPrefix(
      allText, (child.start, child.length), suffixStart
    )

    if (prefixStart != -1) {

      /* There can only be one node with a matching prefix
         so look no further
      */     
      continue = false
      sharedPrefix = Some(child, (prefixStart, prefixLen))
    }
  }

  /* [PC-3]

     This suffix shares nothing with any existing children 
     of node, so insert as a new child of node and quit
  */
  if (sharedPrefix.isEmpty) {

    val newNode = new Node()
    newNode.start = suffixStart
    newNode.length = allText.length - newNode.start
    node.children += newNode
  }
  /* [PC-4]

     There is a child node sharing a common prefix 
     with the suffix
  */
  else {

    /*
      Get the child as 'node', and the length of the prefix
    */    
    val (node, (_, prefixLength)) = sharedPrefix.get

    /* [PC-4.1]

       The prefix is the entire node's value -
       no need to split
    */
    if (node.length == prefixLength)

      /* [PC-5]

         continue on the child node for the remaining
         part of the suffix
      */
      addSuffixToNode(suffixStart + prefixLength, node)

    /* [PC-4.2]

       If the node's value extends beyond the prefix,
       it must be split after the prefix
    */
    else {

      splitNodeOnPrefix(node, prefixLength)

      /* [PC-5]

         continue on the child node for the remaining
         part of the suffix - skip the commonPrefix
         call since there will be none (it would have
         led to an existing child in the current run)
      */
      addSuffixToNode(suffixStart + prefixLength, node, false)

    }
  }
}
</code></pre>
<p>A few remarks:</p>
<ul>
<li><p>The <code>searchPrefix</code> boolean method argument serves to skip the search for a common prefix further up on a branch after we have split a node. It saves a few unnecessary loops, but it's not essential for the algorithm to work correctly.</p>
</li>
<li><p>The <code>@tailrec</code> annotation guarantees the compiler to optimize the recursions not to heap on the stack until a stack overflow error may occur. The <code>@tailrec</code> annotation only compiles if the recursive call is indeed in tail position.</p>
</li>
</ul>
<h3>Finding common prefixes</h3>
<p>Now let's explore the <code>commonPrefix</code> function. It serves to find the common prefix shared by a suffix and a node, if any. It returns a <code>(start,length)</code> pair, referring to the position of the prefix in text <em>T</em> (<code>allText</code>), unless none is found, in which case it returns (-1,0).</p>
<pre><code class="language-java">Example: commonPrefix(text, (2,3), 9) 
         returns (2,3)

  ___    ___
ABCDEFGHACDEFGHABXYZAAABDRHTYEIOEOE ← text
  : :    : :
  : :    9th - 11th character 
  : :
  2nd character and infix length 3
 "CDE"
 
 (NB. infix length is 3, though the common part is longer) 
</code></pre>
<p>So here's the function code. It builds the common prefix string character by character, adding the part found so far to the accumulator argument <code>result</code> and stopping the recursion when the next character doesn't match, or when the loop goes past the infix length or the end of the text.</p>
<pre><code class="language-java">/* commonPrefix finds the common prefix in text 
   for node and suffix.
*/
@tailrec
private def commonPrefix(

   text: String, 
   infix: (Int, Int), 
   suffixStart: Int, 
   result: (Int,Int) = (-1,0) ): (Int,Int) = {

  val (nodeStart, nodeLength) = infix;

  /* If there are no more characters left
     or the prefix would extend beyond the start of
     the suffix, return the prefix
  */
  if (nodeLength == 0 || nodeStart == suffixStart 
      || suffixStart  &gt;= text.length)
    
    result

  /* Otherwise if there is no common prefix (the starting 
     characters don't match), return the empty prefix (-1,0)
  */  
  else if (text(nodeStart) != text(suffixStart))

    result

  /* Otherwise keep moving, save the matching character in the
     accumulator 'result' and recursively advance to the next 
     character.
  */  
  else {

    /* Once a start value is identified, retain it — 
       it must persist through recursions.
    */
    val start = if (result._1 == -1) nodeStart else result._1  

    /* recursively move to the next character 
    */
    commonPrefix(
      text, 
      (nodeStart + 1, nodeLength - 1), 
      suffixStart + 1, 
      (start, result._2 + 1)
    )
  }
}
</code></pre>
<p>The above function provides a mechanism to identify a shared prefix between any two positions in text <em>T</em> without copying the strings, simply by comparing runs of characters starting at the specified positions in text <em>T</em>.</p>
<h3>Splitting a node on a prefix</h3>
<p>The next thing to investigate is how a node is split on a prefix. A picture is worth more than a thousand words:</p>
<pre><code class="language-java">Example: node = ABCDE, 3 children 

           |---child_1 
           |
  ABCDE----|---child_2
           | 
           |---child_3
      
          
Split node ABCDE on prefix AB:  

 node             |---child_1
  :               |
  AB-----CDE------|---child_2 
         :        | 
       newNode    |---child_3
</code></pre>
<p>Essentially, the node is divided immediately after the prefix. This results in one node containing just the prefix and having a single child node that holds the remaining characters, along with the node's children from before the split. Here's the procedure that does exactly that:</p>
<pre><code class="language-java">private def splitNodeOnPrefix(

  node: Node, 
  prefixLen: Int ): Node = {

  val children = node.children
  
  /* get the node's value without the prefix 
  */
  val (postfixStart, postfixLen) = 
    (node.start + prefixLen, node.length - prefixLen)
  
  /* create a new Node from the value obtained in the 
     previous step 
  */
  val newNode = new Node(postfixStart, postfixLen)

  /* add the original node's children to the new node
  */
  newNode.children ++= children
  
  /* shorten the original node to just the prefix length
  */ 
  node.length = prefixLen

  /* remove its children (they are moved to newNode)
  */
  node.children.clear()

  /* add newNode as its child
  */
  node.children += newNode

  /* return the split node */
  node
}
</code></pre>
<p>Nothing too complicated here. On to the final step, getting the output.</p>
<h3>Printing the tree</h3>
<p>We want to be able to view the resulting tree in an intelligible and verifiable way. Here's a depth-first approach to printing the resulting tree. A stack is used instead of recursion to prevent the thing from exploding and minimal formatting indents the child branches.</p>
<pre><code class="language-java">private def printTree(node: Node, text: String): Unit = {

  // Do not use recursion, it might blow up
  val stack = mutable.Stack[(Node, Int)]()
  stack.push((node, 0))

  while (stack.nonEmpty) {
    val (node, depth) = stack.pop()
    println(
      "    " * depth + "|---" + (
        if (node.start == 0 &amp;&amp; node.length == 0) 
          "ROOT"
        else 
          text.substring (
            node.start, 
            Math.min(node.start + node.length, text.length)
          )
      )
    )
    node.children.foreach(c =&gt; stack.push((c,depth+1)))
  }
}
</code></pre>
<p>For input 'abracadabra$' the following output is returned:</p>
<pre><code class="language-java">abracadabra$

|---ROOT
    |---$
    |---dabra$
    |---cadabra$
    |---ra
        |---$
        |---cadabra$
    |---bra
        |---$
        |---cadabra$
    |---a
        |---$
        |---dabra$
        |---cadabra$
        |---bra
            |---$
            |---cadabra$
</code></pre>
<p>And this is exactly what one would expect. This is the output for a correct suffix tree, constructed without an intermediate trie and no characters are stored in the tree itself at any stage.</p>
<h2>How does it perform?</h2>
<p>Finally, we want to know whether it was all worth it. Is the above algorithm indeed more memory efficient and possibly faster than the three step approach with an intermediate trie?</p>
<p>Well, it seems it is indeed. Running a 10,000 character string from a 4 letter alphabet, using the three step algorithm, on my system takes about 25,000 milliseconds on average (based on 10 tests). The same input with the second algorithm takes about 500 milliseconds.</p>
<p>That's about 50 times faster. That's a minute for an hour.</p>
<h3>Worst case performance</h3>
<p>The O(n²) time complexity describes the worst case scenario. What is meant by 'worst case' becomes clear when we compare the performance of two large texts of very different quality that are used as input for the algorithm.</p>
<p>The first 'text' is a sequence of 300,000 characters, again drawn from a 4 letter alphabet (like a DNA code). Due to its limited alphabet this text contains a lot of repetitions. The second text is the complete story <a href="https://www.gutenberg.org/files/36/36-h/36-h.htm">H.G. Wells' War of the Worlds</a>, taken from the Gutenberg site (330,000 characters). As this text is meaningful prose there will be far fewer repetitions.</p>
<p>On the 4 letter alphabet text the algorithm takes around 5~6 minutes to complete. On the H.G. Wells text the algorithm completes in less than 2 seconds.</p>
<p>A huge difference, which can be explained by the fact that because of the many repetitions in the four-letter text, the <code>commonPrefix</code> function on average will take much longer to complete. For each of the n suffixes, the function, as it were, runs much deeper into the n characters of the text. For the Wells text the prefixes will be shorter, cutting of the loop earlier.</p>
<p>(In both cases however, the three step algorithm just chokes on it.)</p>
<p>Overall, that's quite impressive. As a result the smarter algorithm did pass all the tests with flying colours.</p>
<h2>Summary</h2>
<p>A suffix tree is a data structure that enables quick indexing and searching of large texts or strings like DNA sequences. It contains all possible text endings, facilitating rapid pattern searches. However, it requires significantly more memory than the text it represents, and its construction potentially leads to unmanageable memory and time complexity.</p>
<p>To optimize memory usage and processing efficiency, this article describes a simple yet smart algorithm that constructs a suffix tree in a single pass, without first having to construct an intermediate, memory-intensive trie.</p>
<p>Performance tests demonstrate that this optimized algorithm operates significantly faster than the approach with the intermediate trie, achieving a speed improvement of about 50 times for a 10,000 character input.</p>
]]></content:encoded></item></channel></rss>