suffix-array-suffix-tree.md

October 3, 2025 · View on GitHub

Suffix Arrays & Suffix Trees

Estimated Time

  • Reading: 45 minutes
  • Activities: 90 minutes
  • Total: ~2.5 hours

Prerequisites

Motivation

Suffix arrays and suffix trees are powerful data structures used for text processing and pattern matching. They're essential tools for:

  • When you need to quickly find all occurrences of a pattern within a large text (efficient substring searches).
  • For tasks like DNA sequence alignment and gene finding in bioinformatics, where fast pattern matching in long sequences is critical.
  • To identify repeating patterns that can be compressed, making files smaller.
  • To build fast and responsive search functionalities in applications and databases (full-text indexing).
  • For tackling advanced string-related challenges in competitive programming, often requiring optimal time complexity.

These structures offer highly efficient solutions, often achieving near-linear time complexity for complex string operations, which is crucial for large datasets.

Objectives

After completing this lesson, you will be able to:

  1. Explain what suffix arrays and suffix trees are
  2. Understand how these data structures are constructed
  3. Implement a basic suffix array
  4. Use suffix arrays to solve common string problems
  5. Choose the appropriate data structure (suffix array or suffix tree) for specific string problems based on their characteristics and performance.
  6. Analyze the time and space complexity of suffix array and suffix tree construction and operations.

Specific Things to Learn

  • Suffix array definition and properties
  • Naive and efficient suffix array construction
  • Common applications of suffix arrays
  • Suffix tree definition and relationship to suffix arrays
  • Implementing longest common prefix (LCP) arrays
  • Pattern matching with suffix arrays
  • Understanding the trade-offs in space and time complexity for various suffix array and suffix tree operations.
  • Detailed time and space complexity analysis for suffix array construction (naive, efficient) and LCP array construction.
  • Comparative analysis: When to use Suffix Arrays vs. Suffix Trees (advantages, disadvantages, and typical use cases for each).

Activities

Activity 1: Understanding Suffix Arrays

A suffix array is an array of integers representing the starting positions of all suffixes of a string, sorted lexicographically.

For example, consider the string "banana":

  • Suffixes: "banana", "anana", "nana", "ana", "na", "a"
  • Sorted suffixes: "a", "ana", "anana", "banana", "na", "nana"
  • Suffix array: [5, 3, 1, 0, 4, 2] (starting positions of sorted suffixes)

Step-by-Step: Building a Naive Suffix Array

  1. Generate all suffixes of the string
  2. Sort the suffixes lexicographically
  3. Store the starting positions of the sorted suffixes
Naive Suffix Array Implementation (JavaScript)
function buildSuffixArray(text) {
  // Add a terminal character to ensure unique sorting
  text = text + '$';

  // Create array of suffix objects with their starting positions
  const suffixes = [];
  for (let i = 0; i < text.length; i++) {
    suffixes.push({
      index: i,
      suffix: text.substring(i)
    });
  }

  // Sort suffixes lexicographically
  suffixes.sort((a, b) => {
    if (a.suffix < b.suffix) return -1;
    if (a.suffix > b.suffix) return 1;
    return 0;
  });

  // Extract the sorted indices
  const suffixArray = suffixes.map((item) => item.index);

  return suffixArray;
}

// Example usage
const text = 'banana';
const suffixArray = buildSuffixArray(text);
console.log('Text:', text);
console.log('Suffix Array:', suffixArray);

Algorithm Insight: This naive approach has a time complexity of O(N² log N), where N is the length of the string, primarily due to the string comparisons during sorting. Its space complexity is O(N²) if you store all suffixes explicitly, or O(N) if you only store indices and compare on the fly. While simple to understand, this method is generally not used for large strings due to its high complexity.

Try it yourself! Implement the naive suffix array construction for the string "hello" and verify your results.

Activity 2: Efficient Suffix Array Construction

The naive approach has O(n²log(n)) time complexity due to string comparisons during sorting. More efficient algorithms exist:

Prefix Doubling Algorithm

This approach builds the suffix array by iteratively doubling the prefix length used for comparisons.

Prefix Doubling Implementation (Python)
def build_suffix_array(text):
    """Build suffix array using prefix doubling."""
    text = text + '$'
    n = len(text)

    # Initial ranking of characters
    char_to_int = {char: i for i, char in enumerate(sorted(set(text)))}
    rank = [char_to_int[char] for char in text]
    suffix_array = list(range(n))

    # Temporary array for storing new ranks
    new_rank = [0] * n

    # Iterate with increasing k (length of prefix to consider)
    k = 1
    while k < n:
        # Sort by rank pairs (rank[i], rank[i+k])
        # If i+k >= n, use -1 as second rank
        suffix_array.sort(key=lambda i: (rank[i], rank[i + k] if i + k < n else -1))

        # Update ranks
        new_rank[suffix_array[0]] = 0
        for i in range(1, n):
            prev = suffix_array[i-1]
            curr = suffix_array[i]

            # Check if current suffix has same rank pair as previous
            if (rank[curr], rank[curr + k] if curr + k < n else -1) == \
               (rank[prev], rank[prev + k] if prev + k < n else -1):
                new_rank[curr] = new_rank[prev]
            else:
                new_rank[curr] = new_rank[prev] + 1

        rank = new_rank.copy()

        # If all suffixes have unique ranks, we're done
        if rank[suffix_array[-1]] == n - 1:
            break

        k *= 2

    return suffix_array

# Example usage
text = "banana"
suffix_array = build_suffix_array(text)
print(f"Text: {text}")
print(f"Suffix Array: {suffix_array}")

Algorithm Insight: The Prefix Doubling algorithm significantly improves efficiency, achieving a time complexity of O(N log² N). Its space complexity is O(N). This algorithm is a practical choice for building suffix arrays for moderately large strings where O(N log N) or O(N) algorithms are not strictly required or are too complex to implement.

Activity 3: Longest Common Prefix (LCP) Array

The LCP array stores the length of the longest common prefix between adjacent suffixes in the sorted suffix array.

LCP Array Implementation (JavaScript)
function buildLCPArray(text, suffixArray) {
  const n = text.length;

  // Create inverse suffix array
  // This helps us find the position of a suffix in the suffix array
  const inverseSA = new Array(n);
  for (let i = 0; i < n; i++) {
    inverseSA[suffixArray[i]] = i;
  }

  // Initialize LCP array
  const lcp = new Array(n).fill(0);

  // Initialize length of previous LCP
  let k = 0;

  for (let i = 0; i < n; i++) {
    if (inverseSA[i] === n - 1) {
      // The last suffix in sorted order
      k = 0;
      continue;
    }

    // j is the next suffix in sorted order
    const j = suffixArray[inverseSA[i] + 1];

    // Extend the previous LCP value
    while (i + k < n && j + k < n && text[i + k] === text[j + k]) {
      k++;
    }

    lcp[inverseSA[i]] = k;

    // Update k for the next iteration
    if (k > 0) k--;
  }

  return lcp;
}

Algorithm Insight: The LCP array can be constructed in O(N) time after the suffix array is built, using Kasai's algorithm (which is what the provided snippet implements). Its space complexity is O(N). The LCP array is crucial for many advanced string algorithms, such as finding repeated substrings, longest common substrings between two strings, and counting distinct substrings, as it provides valuable information about commonalities between suffixes.

Activity 4: Pattern Matching with Suffix Arrays

Let's implement a function to find all occurrences of a pattern in a text using a suffix array:

Pattern Matching Implementation (Python)
def find_pattern(text, pattern, suffix_array):
    """Find all occurrences of pattern in text using suffix array."""
    n = len(text)
    m = len(pattern)

    # Binary search to find the lower bound
    left, right = 0, n - 1
    first_occurrence = -1

    while left <= right:
        mid = (left + right) // 2
        suffix_start = suffix_array[mid]
        suffix = text[suffix_start:suffix_start + m]

        if suffix >= pattern:
            right = mid - 1
            if suffix.startswith(pattern):
                first_occurrence = mid
        else:
            left = mid + 1

    if first_occurrence == -1:
        return []  # Pattern not found

    # Binary search to find the upper bound
    left, right = first_occurrence, n - 1
    last_occurrence = first_occurrence

    while left <= right:
        mid = (left + right) // 2
        suffix_start = suffix_array[mid]
        suffix = text[suffix_start:suffix_start + m]

        if suffix.startswith(pattern):
            last_occurrence = mid
            left = mid + 1
        else:
            right = mid - 1

    # Return all occurrences
    return [suffix_array[i] for i in range(first_occurrence, last_occurrence + 1)]

# Example usage
text = "banana"
pattern = "na"
suffix_array = build_suffix_array(text)[:-1]  # Remove the terminal character
occurrences = find_pattern(text, pattern, suffix_array)
print(f"Pattern '{pattern}' found at positions: {occurrences}")

Algorithm Insight: Pattern Matching with Suffix Arrays" heading, add: "Searching for a pattern of length M in a text of length N using a suffix array takes O(M log N) time, thanks to binary search. This is highly efficient for repeated pattern searches on a fixed text. This method is ideal for scenarios where you have a large text and need to perform many quick searches for different patterns, as the suffix array is built once and then reused.

Activity 5: Introduction to Suffix Trees

A suffix tree is a compressed trie containing all suffixes of a string. Each suffix tree has:

  • Root node
  • Internal nodes (representing common prefixes)
  • Leaf nodes (representing complete suffixes)
  • Edges labeled with substrings

Suffix trees can be built from suffix arrays, but direct construction algorithms like Ukkonen's algorithm are more common.

Basic Suffix Tree Node Structure (Java)
class SuffixTreeNode {
    Map<Character, SuffixTreeNode> children;
    int startIndex;
    int endIndex;

    public SuffixTreeNode() {
        children = new HashMap<>();
        startIndex = -1;
        endIndex = -1;
    }

    public boolean isLeaf() {
        return children.isEmpty();
    }

    public void addChild(char c, SuffixTreeNode node) {
        children.put(c, node);
    }

    public SuffixTreeNode getChild(char c) {
        return children.get(c);
    }

    public boolean hasChild(char c) {
        return children.containsKey(c);
    }
}

Algorithm Insight: Suffix trees can be constructed in O(N) time and O(N) space (using algorithms like Ukkonen's). This makes them theoretically optimal for many string problems. While more complex to implement than suffix arrays, suffix trees offer faster performance (often O(1) or O(M)) for certain operations like exact pattern matching, finding all occurrences of a pattern, or finding the longest common substring, especially when the string is very large and constant-time factors matter.

Activity 6: Practical Applications

Longest Repeated Substring

Using suffix arrays and LCP arrays, we can find the longest repeated substring in a text:

Longest Repeated Substring Implementation (JavaScript)
function longestRepeatedSubstring(text, suffixArray, lcpArray) {
  let maxLength = 0;
  let maxIndex = 0;

  for (let i = 0; i < lcpArray.length; i++) {
    if (lcpArray[i] > maxLength) {
      maxLength = lcpArray[i];
      maxIndex = i;
    }
  }

  if (maxLength === 0) {
    return 'No repeated substring found';
  }

  // The longest repeated substring starts at suffixArray[maxIndex]
  return text.substring(
    suffixArray[maxIndex],
    suffixArray[maxIndex] + maxLength
  );
}

// Example usage
const text = 'banana';
const suffixArray = buildSuffixArray(text);
const lcpArray = buildLCPArray(text, suffixArray);
const longestRepeated = longestRepeatedSubstring(text, suffixArray, lcpArray);
console.log('Longest repeated substring:', longestRepeated);

Algorithm Insight: This problem can be solved in O(N) time after the suffix array and LCP array are constructed, making it very efficient for identifying redundancies in large texts.

Longest Common Substring

Finding the longest common substring between two strings using suffix arrays:

Longest Common Substring Implementation (Python)
def longest_common_substring(s1, s2):
    """Find the longest common substring between s1 and s2 using suffix arrays."""
    # Concatenate strings with a unique separator
    combined = s1 + '#' + s2 + '$'
    n1, n2 = len(s1), len(s2)
    n = len(combined)

    # Build suffix array for combined string
    suffix_array = build_suffix_array(combined)

    # Build LCP array
    lcp_array = build_lcp_array(combined, suffix_array)

    # Find the maximum LCP between suffixes from different strings
    max_length = 0
    max_index = 0

    for i in range(1, n):
        # Check if adjacent suffixes in suffix array come from different strings
        curr_suffix = suffix_array[i]
        prev_suffix = suffix_array[i-1]

        # One suffix from s1, one from s2
        if (curr_suffix < n1 and prev_suffix > n1) or (curr_suffix > n1 and prev_suffix < n1):
            if lcp_array[i-1] > max_length:
                max_length = lcp_array[i-1]
                max_index = min(curr_suffix, prev_suffix)

    if max_length == 0:
        return "No common substring found"

    return combined[max_index:max_index + max_length]

Algorithm Insight: Finding the longest common substring between two strings using suffix arrays and LCP arrays can be done in O(N1 + N2) time (where N1 and N2 are lengths of the strings), which is highly efficient for bioinformatics applications and plagiarism detection.

Activity 7: Challenge Problems

Try solving these problems using suffix arrays or suffix trees:

  1. Find all unique substrings of a string
  2. Implement a function to check if one string is a substring of another
  3. Find the lexicographically smallest rotation of a string
  4. Implement a basic text search engine using suffix arrays

Additional Resources