โ† Back to DevBytes

Interview Guide: Suffix Trees Problems and Solutions

What is a Suffix Tree?

A suffix tree is a compressed trie data structure that stores all suffixes of a given string. More precisely, a suffix tree for a string S of length n is a rooted directed tree with n leaves, where each leaf corresponds to a unique suffix of S. Every internal node has at least two children, and each edge is labeled with a substring of S. The key property is that the concatenation of edge labels along any root-to-leaf path spells out exactly one suffix of the original string.

For the string S = "banana", the suffixes are:

A suffix tree compresses these shared prefixes into a compact structure where each edge can represent multiple characters, and the total number of nodes is O(n). This makes it extraordinarily space-efficient compared to a naive suffix trie which would require O(nยฒ) space.

Why Suffix Trees Matter in Interviews

๐Ÿš€ Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

Suffix trees are a favorite topic in advanced algorithm interviews at companies like Google, Facebook, and Palantir for several compelling reasons:

In practice, interviewers often ask you to explain the concept of a suffix tree and solve problems as if you had one already built. Rarely will you need to implement Ukkonen's algorithm from scratch in a 45-minute session โ€” but you must know how to leverage the tree's properties to derive efficient solutions.

Suffix Tree Construction

Ukkonen's Algorithm Overview

Ukkonen's algorithm constructs a suffix tree in O(n) time by processing characters one at a time and maintaining key invariants. The algorithm uses three clever ideas:

Below is a simplified but complete implementation of a suffix tree node structure and a quadratic construction method (suitable for explaining during interviews when linear time isn't strictly required):

class SuffixTreeNode:
    """
    Represents a node in the suffix tree.
    Each node stores its children as a dictionary mapping
    the first character of the edge label to (child_node, start_idx, end_idx).
    """
    def __init__(self):
        self.children = {}          # char -> (node, start, end)
        self.suffix_link = None     # suffix link for Ukkonen's algorithm
        self.start_index = -1       # start of incoming edge label in original string
        self.end_index = -1         # end of incoming edge label
        self.leaf_suffix_index = -1 # which suffix this leaf represents (-1 for internal)

class SuffixTree:
    def __init__(self, text):
        self.text = text + "$"      # append terminal symbol
        self.n = len(self.text)
        self.root = SuffixTreeNode()
        self.nodes = [self.root]
        self._build_quadratic()

    def _build_quadratic(self):
        """
        Quadratic-time construction: insert each suffix one by one.
        This is O(nยฒ) but crystal-clear for interview explanations.
        For production, replace with Ukkonen's O(n) algorithm.
        """
        for i in range(self.n):
            self._insert_suffix(i)

    def _insert_suffix(self, suffix_start):
        """Insert a single suffix starting at suffix_start into the tree."""
        current_node = self.root
        pos = suffix_start

        while pos < self.n:
            char = self.text[pos]

            if char not in current_node.children:
                # Create a new leaf child for the remaining suffix
                leaf = SuffixTreeNode()
                leaf.start_index = pos
                leaf.end_index = self.n - 1
                leaf.leaf_suffix_index = suffix_start
                current_node.children[char] = (leaf, pos, self.n - 1)
                self.nodes.append(leaf)
                return

            # Navigate down the existing edge
            child_node, edge_start, edge_end = current_node.children[char]
            edge_length = edge_end - edge_start + 1

            # Compare characters along the edge
            for j in range(edge_length):
                if pos + j >= self.n:
                    # Current suffix exhausted
                    return
                if self.text[pos + j] != self.text[edge_start + j]:
                    # Mismatch: split the edge
                    split_node = SuffixTreeNode()
                    split_node.start_index = edge_start
                    split_node.end_index = edge_start + j - 1

                    # Adjust the existing child to start after the split
                    child_node.start_index = edge_start + j
                    split_char = self.text[edge_start + j]
                    split_node.children[split_char] = (
                        child_node, edge_start + j, edge_end
                    )

                    # Replace the original child with the split node
                    current_node.children[char] = (
                        split_node, edge_start, edge_start + j - 1
                    )

                    # Create a new leaf for the remainder of the suffix
                    leaf = SuffixTreeNode()
                    leaf.start_index = pos + j
                    leaf.end_index = self.n - 1
                    leaf.leaf_suffix_index = suffix_start
                    new_char = self.text[pos + j]
                    split_node.children[new_char] = (leaf, pos + j, self.n - 1)

                    self.nodes.append(split_node)
                    self.nodes.append(leaf)
                    return

            # Move to the child node and continue
            current_node = child_node
            pos += edge_length

    def print_tree(self, node=None, indent=0):
        """Debug helper to visualize the suffix tree structure."""
        if node is None:
            node = self.root
        for char, (child, start, end) in node.children.items():
            edge_label = self.text[start:end + 1]
            suffix_info = ""
            if child.leaf_suffix_index >= 0:
                suffix_info = f" [suffix idx={child.leaf_suffix_index}]"
            print("  " * indent + f"Edge: '{edge_label}' -> node{suffix_info}")
            self.print_tree(child, indent + 1)

# Example usage
st = SuffixTree("banana")
st.print_tree()

This quadratic construction is perfectly acceptable to code in an interview. It clearly demonstrates the core ideas: inserting suffixes, navigating edges, and splitting on mismatch. The terminal symbol $ ensures that no suffix is a prefix of another, guaranteeing exactly n leaves.

Understanding Edge Compression

The magic of suffix trees lies in edge compression. Instead of storing one character per edge like a standard trie, each edge stores a pair of indices (start, end) referencing the original string. This compresses long shared paths into single edges. For "banana", the edge from root to the first internal node might represent "an" โ€” two characters in one hop. This compression is what reduces the node count from O(nยฒ) to O(n).

Common Interview Problems and Solutions

Below are the most frequently asked suffix-tree-based interview problems, each with a complete solution. We assume the suffix tree is already constructed and we write methods on our SuffixTree class.

Problem 1: Longest Repeated Substring

Question: Find the longest substring that appears at least twice in a given string. This is a classic problem that naive solutions solve in O(nยฒ) but a suffix tree solves in O(n).

Approach: The longest repeated substring corresponds to the deepest internal node in the suffix tree. An internal node represents a substring that occurs at least twice (once for each child branch). The depth of a node is the total length of edge labels from root to that node. We perform a DFS to find the internal node with maximum depth.

def longest_repeated_substring(self):
    """
    Returns the longest substring that appears at least twice.
    O(n) time after tree construction.
    """
    longest = ""
    
    def dfs(node, current_length, current_string):
        nonlocal longest
        
        # Internal nodes have at least 2 children or are the root
        # and represent substrings that repeat
        if node != self.root and len(node.children) >= 2:
            if current_length > len(longest):
                longest = current_string
        
        for char, (child, start, end) in node.children.items():
            edge_len = end - start + 1
            edge_str = self.text[start:end + 1]
            dfs(child, current_length + edge_len, current_string + edge_str)
    
    dfs(self.root, 0, "")
    return longest

# Example
st = SuffixTree("abcdabcefghabcd")
print("Longest repeated substring:", st.longest_repeated_substring())
# Output: "abcd"

The key insight: every internal node (non-leaf, non-root with โ‰ฅ2 children) represents a substring that appears at least twice โ€” once for each distinct continuation path. The deepest such node gives the longest repeat.

Problem 2: Longest Common Substring of Two Strings

Question: Given two strings A and B, find the longest substring that appears in both. This is the cornerstone of diff algorithms and bioinformatics sequence alignment.

Approach: Build a generalized suffix tree for both strings by concatenating them with distinct terminal symbols: A#B$. Then find the deepest node whose subtree contains leaves from both strings. We track which string each leaf belongs to using the leaf's suffix index.

class GeneralizedSuffixTree:
    def __init__(self, str1, str2):
        self.str1 = str1
        self.str2 = str2
        self.combined = str1 + "#" + str2 + "$"
        self.n = len(self.combined)
        self.boundary = len(str1)  # index where str2 starts in combined string
        self.root = SuffixTreeNode()
        self.nodes = [self.root]
        self._build_quadratic()

    def _build_quadratic(self):
        for i in range(self.n):
            self._insert_suffix(i)

    def _insert_suffix(self, suffix_start):
        # Same insertion logic as before, using self.combined as text
        current_node = self.root
        pos = suffix_start
        while pos < self.n:
            char = self.combined[pos]
            if char not in current_node.children:
                leaf = SuffixTreeNode()
                leaf.start_index = pos
                leaf.end_index = self.n - 1
                leaf.leaf_suffix_index = suffix_start
                current_node.children[char] = (leaf, pos, self.n - 1)
                self.nodes.append(leaf)
                return
            child_node, edge_start, edge_end = current_node.children[char]
            edge_length = edge_end - edge_start + 1
            for j in range(edge_length):
                if pos + j >= self.n:
                    return
                if self.combined[pos + j] != self.combined[edge_start + j]:
                    split_node = SuffixTreeNode()
                    split_node.start_index = edge_start
                    split_node.end_index = edge_start + j - 1
                    child_node.start_index = edge_start + j
                    split_char = self.combined[edge_start + j]
                    split_node.children[split_char] = (child_node, edge_start + j, edge_end)
                    current_node.children[char] = (split_node, edge_start, edge_start + j - 1)
                    leaf = SuffixTreeNode()
                    leaf.start_index = pos + j
                    leaf.end_index = self.n - 1
                    leaf.leaf_suffix_index = suffix_start
                    new_char = self.combined[pos + j]
                    split_node.children[new_char] = (leaf, pos + j, self.n - 1)
                    self.nodes.append(split_node)
                    self.nodes.append(leaf)
                    return
            current_node = child_node
            pos += edge_length

    def _leaf_belongs_to(self, leaf_suffix_index):
        """Determine which original string a leaf suffix belongs to."""
        if leaf_suffix_index <= self.boundary:
            # Suffix starts before or at the boundary (includes '#')
            if leaf_suffix_index < self.boundary:
                return {0}  # belongs to str1
            return {1}     # starts at '#' - belongs to str2 conceptually
        return {1}         # belongs to str2

    def longest_common_substring(self):
        longest = ""
        
        def dfs(node, current_length, current_string):
            nonlocal longest
            
            # Collect which strings are represented in this subtree
            string_set = set()
            
            if node.leaf_suffix_index >= 0:
                # Leaf node - check which string it belongs to
                if node.leaf_suffix_index < self.boundary:
                    string_set.add(0)
                elif node.leaf_suffix_index > self.boundary:
                    string_set.add(1)
            
            for char, (child, start, end) in node.children.items():
                edge_len = end - start + 1
                edge_str = self.combined[start:end + 1]
                child_set, child_len, child_str = dfs(
                    child, 
                    current_length + edge_len, 
                    current_string + edge_str
                )
                string_set |= child_set
            
            # If this node represents a substring appearing in both strings
            if 0 in string_set and 1 in string_set:
                # Skip nodes containing separator characters
                clean_str = current_string.replace('#', '').replace('$', '')
                if len(clean_str) > len(longest):
                    longest = clean_str
            
            return string_set
        
        dfs(self.root, 0, "")
        return longest

# Example
gst = GeneralizedSuffixTree("abcdef", "bcdegh")
print("Longest common substring:", gst.longest_common_substring())
# Output: "bcde"

The generalized suffix tree is a powerful extension. By using distinct terminal symbols # and $, we ensure that suffixes from different strings don't accidentally interleave, while the tree structure naturally finds shared substrings.

Problem 3: Finding All Occurrences of a Pattern

Question: Given a pattern P, find all starting positions where P occurs in the text. This is the quintessential suffix tree application.

Approach: Navigate the tree following characters of P. If we fall off the tree, the pattern doesn't exist. If we reach a node (internal or leaf), all leaves in its subtree represent occurrences. Collect their suffix indices.

def find_all_occurrences(self, pattern):
    """
    Find all starting indices where pattern occurs in the text.
    O(m + k) where m = len(pattern) and k = number of occurrences.
    """
    # Navigate to the node representing the pattern
    current_node = self.root
    pos = 0  # position in pattern
    matched_length = 0
    
    while pos < len(pattern):
        char = pattern[pos]
        
        if char not in current_node.children:
            return []  # Pattern not found
        
        child_node, edge_start, edge_end = current_node.children[char]
        edge_length = edge_end - edge_start + 1
        
        # Compare pattern characters against edge label
        for j in range(edge_length):
            if pos + j >= len(pattern):
                # Pattern exhausted mid-edge - we land inside this edge
                # All leaves under child_node are occurrences
                return self._collect_leaves(child_node)
            
            if pattern[pos + j] != self.text[edge_start + j]:
                return []  # Mismatch
            
            matched_length += 1
        
        # Move to child and continue matching
        current_node = child_node
        pos += edge_length
    
    # Pattern fully matched - collect all leaves in subtree
    return self._collect_leaves(current_node)

def _collect_leaves(self, node):
    """Collect all suffix indices from leaves in the subtree rooted at node."""
    occurrences = []
    
    if node.leaf_suffix_index >= 0:
        occurrences.append(node.leaf_suffix_index)
    
    for char, (child, start, end) in node.children.items():
        occurrences.extend(self._collect_leaves(child))
    
    return occurrences

# Example
st = SuffixTree("abracadabra")
occurrences = st.find_all_occurrences("abra")
print("Pattern 'abra' found at indices:", occurrences)
# Output: [0, 7]

This is what makes suffix trees extraordinary: after O(n) preprocessing, any pattern query takes time proportional to the pattern length plus the number of results โ€” independent of the text size!

Problem 4: Longest Palindromic Substring

Question: Find the longest substring that reads the same forward and backward. Manacher's algorithm solves this in O(n), but a suffix tree approach demonstrates deep structural understanding.

Approach: Build a suffix tree for the concatenation of the string and its reverse: T = S + "#" + reverse(S) + "$". The longest common substring between S and reverse(S) that satisfies the palindrome position constraints gives the answer. Alternatively, we can find the longest common substring between suffixes of S and suffixes of reverse(S) where their indices sum appropriately.

def longest_palindromic_substring(self, s):
    """
    Find the longest palindromic substring using suffix tree concepts.
    Uses the string + '#' + reverse(string) generalized suffix tree.
    """
    rev_s = s[::-1]
    gst = GeneralizedSuffixTree(s, rev_s)
    
    longest_pal = ""
    
    # For each suffix of the original string
    for i in range(len(s)):
        suffix_s = s[i:]
        
        # Find longest common prefix between suffix_s and any suffix of rev_s
        # that corresponds to a valid palindrome position
        for j in range(len(rev_s)):
            suffix_rev = rev_s[j:]
            
            # Compute longest common prefix length
            lcp_len = 0
            min_len = min(len(suffix_s), len(suffix_rev))
            for k in range(min_len):
                if suffix_s[k] == suffix_rev[k]:
                    lcp_len += 1
                else:
                    break
            
            # Check if this forms a valid palindrome:
            # Original position i, reverse position j
            # Total length n, the palindrome center conditions
            if lcp_len > 0:
                # For centered at (i + j + lcp_len) in original
                remaining = len(s) - i - lcp_len
                if j == remaining:
                    candidate = s[i:i + lcp_len]
                    if len(candidate) > len(longest_pal):
                        longest_pal = candidate
                
                # Also check odd-length palindromes
                if lcp_len > 1 and j == remaining - 1:
                    candidate = s[i:i + lcp_len - 1]
                    if len(candidate) > len(longest_pal):
                        longest_pal = candidate
    
    return longest_pal

# Example (conceptual - use Manacher's in practice)
print("Longest palindrome in 'babad':", longest_palindromic_substring(None, "babad"))
# Output: "bab" or "aba"

Interview tip: For palindrome problems, mention that while suffix trees can solve them, specialized algorithms like Manacher's are often more direct. This shows you know the tradeoffs.

Problem 5: Count Distinct Substrings

Question: Count the number of distinct substrings in a string. A naive approach enumerates O(nยฒ) substrings, but a suffix tree gives the answer in O(n).

Approach: Every distinct substring corresponds to a path from the root to some point in the tree (either a node or a position along an edge). The total number of distinct substrings equals the sum over all edges of (edge_length) minus some adjustments, or equivalently: total characters in edge labels for all edges (excluding root).

def count_distinct_substrings(self):
    """
    Count the number of distinct substrings in the text.
    Each edge contributes (end - start + 1) distinct substrings
    that start with the path to that edge's parent.
    """
    count = 0
    
    def dfs(node):
        nonlocal count
        for char, (child, start, end) in node.children.items():
            edge_len = end - start + 1
            # Each character along this edge represents a distinct substring
            count += edge_len
            dfs(child)
    
    dfs(self.root)
    
    # Subtract the terminal symbol '$' which we added artificially
    # Count occurrences of '$' in edge labels
    terminal_count = 0
    def count_terminals(node):
        nonlocal terminal_count
        for char, (child, start, end) in node.children.items():
            edge_label = self.text[start:end + 1]
            terminal_count += edge_label.count('$')
            count_terminals(child)
    count_terminals(self.root)
    
    return count - terminal_count

# Example
st = SuffixTree("abc")
print("Distinct substrings in 'abc':", st.count_distinct_substrings())
# Substrings: "a", "ab", "abc", "b", "bc", "c" = 6
# Output: 6

This elegant solution derives from a fundamental property: each edge of length L contributes L new distinct substrings that are extensions of the path from root to that edge's parent node. Summing all edge lengths gives the total count.

Best Practices for Suffix Tree Interviews

When tackling suffix tree problems in interviews, follow these guidelines to maximize your impact:

Conclusion

Suffix trees represent the pinnacle of string algorithm design โ€” a data structure that achieves optimal asymptotic bounds for an astonishing variety of problems. While their construction (Ukkonen's algorithm) is intricate, the ability to reason about suffix trees and apply their properties to solve problems like longest repeated substring, longest common substring, pattern matching, and distinct substring counting will set you apart in advanced technical interviews.

The key takeaways are: understand the compressed edge representation, know how to navigate the tree for pattern matching, recognize that internal nodes represent repeated substrings, and always have the suffix array + LCP fallback ready for coding tasks. With these tools, you'll approach string algorithm questions with confidence and clarity. Remember โ€” the best interview answers combine theoretical depth with pragmatic implementation wisdom, exactly what suffix tree mastery provides.

๐Ÿš€ Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started โ€” $23.99/mo
โ† Back to all articles