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:
- Explain what suffix arrays and suffix trees are
- Understand how these data structures are constructed
- Implement a basic suffix array
- Use suffix arrays to solve common string problems
- Choose the appropriate data structure (suffix array or suffix tree) for specific string problems based on their characteristics and performance.
- 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
- Generate all suffixes of the string
- Sort the suffixes lexicographically
- 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.
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:
- Find all unique substrings of a string
- Implement a function to check if one string is a substring of another
- Find the lexicographically smallest rotation of a string
- Implement a basic text search engine using suffix arrays