String Algorithms

Algorithm Preprocessing Matching Space Best For
Naive O(1) O(n×m) O(1) Short patterns, teaching
KMP O(m) O(n) O(m) Single pattern, no backtracking
Rabin-Karp O(m) O(n+m) avg O(1) Multiple patterns, plagiarism detection
Boyer-Moore O(m+σ) O(n/m) best O(m+σ) Large texts, text editors (σ = alphabet size)
Z Algorithm O(m) O(n+m) O(n+m) Pattern matching variants
Aho-Corasick O(∑m) O(n+k) O(∑m) Multiple patterns simultaneously (k = matches)
Manacher - O(n) O(n) Longest palindromic substring
Suffix Array O(n log n) O(m log n) O(n) Multiple searches, genome analysis

1. Naive Pattern Matching Brute Force

Aspect Description Characteristic
Algorithm Try all possible alignments of pattern in text Check character by character at each position
Time Complexity O((n-m+1) × m) = O(n×m) Worst when many partial matches
Best Case O(n) when first character never matches Early mismatch eliminates position quickly
Worst Case Pattern: "AAA", Text: "AAAAAAA" Maximum comparisons at each position
Space Complexity O(1) No extra data structures needed
Use Cases Very short patterns, educational purposes Not practical for production systems

Example: Naive Pattern Matching

function naivePatternMatch(text, pattern) {
    const n = text.length;
    const m = pattern.length;
    const matches = [];
    
    // Try each possible starting position
    for (let i = 0; i <= n - m; i++) {
        let j = 0;
        
        // Check if pattern matches at position i
        while (j < m && text[i + j] === pattern[j]) {
            j++;
        }
        
        // If full pattern matched
        if (j === m) {
            matches.push(i);
        }
    }
    
    return matches;
}

console.log(naivePatternMatch("AABAACAADAABAABA", "AABA"));
// Output: [0, 9, 12] - All occurrences found

console.log(naivePatternMatch("ABCDEFGHIJ", "DEF"));
// Output: [3]

// Time: O(n×m) worst case
// Example: Pattern "AAA" in "AAAAAAA" requires many comparisons

2. KMP Algorithm Partial Match Table

Concept Description Advantage
LPS Array Longest Proper Prefix which is also Suffix Avoid re-checking matched characters
No Backtracking Text pointer never moves backward Efficient for streaming data
Preprocessing Build LPS array from pattern in O(m) One-time cost, reusable for multiple searches
Matching Use LPS to skip unnecessary comparisons Linear time O(n) guaranteed
Total Complexity O(n + m) Optimal for single pattern matching
Invented By Knuth, Morris, Pratt (1977) First linear-time string matching

Example: KMP Algorithm

function kmpSearch(text, pattern) {
    const n = text.length;
    const m = pattern.length;
    
    // Build LPS (Longest Proper Prefix which is Suffix) array
    const lps = buildLPS(pattern);
    const matches = [];
    
    let i = 0; // Index for text
    let j = 0; // Index for pattern
    
    while (i < n) {
        if (text[i] === pattern[j]) {
            i++;
            j++;
        }
        
        if (j === m) {
            // Pattern found at index (i - j)
            matches.push(i - j);
            j = lps[j - 1]; // Continue searching
        } else if (i < n && text[i] !== pattern[j]) {
            if (j !== 0) {
                // Don't match lps[0..lps[j-1]] characters
                j = lps[j - 1];
            } else {
                i++;
            }
        }
    }
    
    return matches;
}

function buildLPS(pattern) {
    const m = pattern.length;
    const lps = new Array(m).fill(0);
    let len = 0; // Length of previous longest prefix suffix
    let i = 1;
    
    while (i < m) {
        if (pattern[i] === pattern[len]) {
            len++;
            lps[i] = len;
            i++;
        } else {
            if (len !== 0) {
                len = lps[len - 1];
            } else {
                lps[i] = 0;
                i++;
            }
        }
    }
    
    return lps;
}

// Example with LPS explanation
const pattern = "ABABCABAB";
const lps = buildLPS(pattern);
console.log("Pattern:", pattern);
console.log("LPS:    ", lps);
// Output: [0, 0, 1, 2, 0, 1, 2, 3, 4]
// Meaning: At index 4 (C), no prefix=suffix
//          At index 8 (B), "ABAB" is both prefix and suffix

console.log(kmpSearch("ABABDABACDABABCABAB", "ABABCABAB"));
// Output: [10] - Pattern found at index 10
// Time: O(n + m), Space: O(m)
Note: KMP never backtracks in the text, making it ideal for streaming data. The LPS array tells us how many characters we can skip when a mismatch occurs, utilizing information from previous matches.

3. Rabin-Karp Rolling Hash Pattern

Feature Description Details
Hash Function Treat string as number in base d hash = (c₀×d^(m-1) + c₁×d^(m-2) + ... + c_(m-1)) mod q
Rolling Hash Update hash in O(1) by removing/adding character Remove leading, add trailing, no recalculation
Spurious Hits Hash match doesn't guarantee string match Must verify with character comparison
Average Time O(n + m) With good hash, collisions rare
Worst Time O(n×m) When all hashes collide (unlikely)
Multiple Patterns Can search for k patterns in O(n + k×m) Calculate hash for each pattern

Example: Rabin-Karp Algorithm

function rabinKarp(text, pattern) {
    const n = text.length;
    const m = pattern.length;
    const d = 256; // Number of characters in alphabet
    const q = 101; // Prime number for modulo
    
    let patternHash = 0; // Hash value for pattern
    let textHash = 0;    // Hash value for text window
    let h = 1;           // d^(m-1) % q
    const matches = [];
    
    // Calculate h = d^(m-1) % q
    for (let i = 0; i < m - 1; i++) {
        h = (h * d) % q;
    }
    
    // Calculate initial hash values
    for (let i = 0; i < m; i++) {
        patternHash = (d * patternHash + pattern.charCodeAt(i)) % q;
        textHash = (d * textHash + text.charCodeAt(i)) % q;
    }
    
    // Slide pattern over text
    for (let i = 0; i <= n - m; i++) {
        // Check if hash values match
        if (patternHash === textHash) {
            // Verify character by character (avoid spurious hits)
            let match = true;
            for (let j = 0; j < m; j++) {
                if (text[i + j] !== pattern[j]) {
                    match = false;
                    break;
                }
            }
            if (match) matches.push(i);
        }
        
        // Calculate hash for next window (rolling hash)
        if (i < n - m) {
            textHash = (d * (textHash - text.charCodeAt(i) * h) + 
                       text.charCodeAt(i + m)) % q;
            
            // Make sure hash is positive
            if (textHash < 0) textHash += q;
        }
    }
    
    return matches;
}

console.log(rabinKarp("GEEKS FOR GEEKS", "GEEKS"));
// Output: [0, 10]

console.log(rabinKarp("AABAACAADAABAABA", "AABA"));
// Output: [0, 9, 12]

// Average: O(n + m), Worst: O(n×m) but unlikely with good hash

Example: Multiple Pattern Matching with Rabin-Karp

function multiPatternRabinKarp(text, patterns) {
    const d = 256;
    const q = 101;
    const results = {};
    
    // Calculate hashes for all patterns
    const patternHashes = new Map();
    for (const pattern of patterns) {
        let hash = 0;
        for (let i = 0; i < pattern.length; i++) {
            hash = (d * hash + pattern.charCodeAt(i)) % q;
        }
        patternHashes.set(pattern, hash);
        results[pattern] = [];
    }
    
    // Search for each pattern length
    const lengths = [...new Set(patterns.map(p => p.length))];
    
    for (const m of lengths) {
        let h = 1;
        for (let i = 0; i < m - 1; i++) {
            h = (h * d) % q;
        }
        
        let textHash = 0;
        for (let i = 0; i < m; i++) {
            textHash = (d * textHash + text.charCodeAt(i)) % q;
        }
        
        for (let i = 0; i <= text.length - m; i++) {
            // Check all patterns of this length
            for (const pattern of patterns) {
                if (pattern.length === m && 
                    patternHashes.get(pattern) === textHash) {
                    if (text.substring(i, i + m) === pattern) {
                        results[pattern].push(i);
                    }
                }
            }
            
            if (i < text.length - m) {
                textHash = (d * (textHash - text.charCodeAt(i) * h) + 
                           text.charCodeAt(i + m)) % q;
                if (textHash < 0) textHash += q;
            }
        }
    }
    
    return results;
}

const text = "ABABCABCABABABD";
const patterns = ["AB", "ABC", "CAB"];
console.log(multiPatternRabinKarp(text, patterns));
// Output: {AB: [0, 2, 5, 9, 11, 13], ABC: [2, 5], CAB: [4, 7]}

4. Z Algorithm Pattern Matching

Concept Description Property
Z Array Z[i] = length of longest substring starting from i which is also prefix Z[i] tells how much of string matches from position i
Construction Build Z array in linear time using [L, R] interval Maintains rightmost matched segment
Pattern Matching Concatenate pattern$text and build Z array Z[i] = m means pattern found at position i
Time Complexity O(n + m) Linear time, single pass
Space Complexity O(n + m) Store concatenated string and Z array
Applications Pattern matching, finding repeating patterns Simpler than KMP for some problems

Example: Z Algorithm

function buildZArray(str) {
    const n = str.length;
    const z = new Array(n).fill(0);
    
    let l = 0, r = 0;
    
    for (let i = 1; i < n; i++) {
        // If i is outside current [l, r], compute z[i] naively
        if (i > r) {
            l = r = i;
            while (r < n && str[r - l] === str[r]) {
                r++;
            }
            z[i] = r - l;
            r--;
        } else {
            // i is inside [l, r]
            const k = i - l;
            
            // If z[k] < r - i + 1, we can reuse z[k]
            if (z[k] < r - i + 1) {
                z[i] = z[k];
            } else {
                // Start matching from r + 1
                l = i;
                while (r < n && str[r - l] === str[r]) {
                    r++;
                }
                z[i] = r - l;
                r--;
            }
        }
    }
    
    return z;
}

function zAlgorithmSearch(text, pattern) {
    // Concatenate pattern and text with special character
    const concat = pattern + "$" + text;
    const z = buildZArray(concat);
    
    const m = pattern.length;
    const matches = [];
    
    // Find all positions where z[i] equals pattern length
    for (let i = 0; i < z.length; i++) {
        if (z[i] === m) {
            // Found match at position (i - m - 1) in original text
            matches.push(i - m - 1);
        }
    }
    
    return matches;
}

// Demonstrate Z array
const str = "aabcaabxaaaz";
const z = buildZArray(str);
console.log("String:", str);
console.log("Z Array:", z);
// Output: [0, 1, 0, 0, 3, 1, 0, 0, 2, 2, 1, 0]
// Meaning: z[4]=3 means "aab" matches from index 4

console.log(zAlgorithmSearch("ABABDABACDABABCABAB", "ABABCABAB"));
// Output: [10]

// Time: O(n + m), Space: O(n + m)
Heuristic Description Advantage
Bad Character Skip alignments based on mismatched character Jump by max(1, j - last occurrence)
Good Suffix Use matched suffix to determine shift Larger shifts when suffix matches elsewhere
Right-to-Left Compare pattern from right to left Detect mismatches faster in practice
Best Case O(n/m) - sublinear! Can skip multiple characters at once
Worst Case O(n×m) Rare with random text
Practical Use Fastest for natural language text Used in grep, text editors

Example: Boyer-Moore (Bad Character Heuristic)

function boyerMooreSearch(text, pattern) {
    const n = text.length;
    const m = pattern.length;
    
    // Build bad character table
    const badChar = buildBadCharTable(pattern);
    const matches = [];
    
    let s = 0; // Shift of pattern with respect to text
    
    while (s <= n - m) {
        let j = m - 1;
        
        // Keep reducing j while characters match
        // Compare from RIGHT to LEFT
        while (j >= 0 && pattern[j] === text[s + j]) {
            j--;
        }
        
        if (j < 0) {
            // Pattern found
            matches.push(s);
            
            // Shift pattern to align next occurrence
            s += (s + m < n) ? m - badChar[text.charCodeAt(s + m)] : 1;
        } else {
            // Shift pattern using bad character heuristic
            const badCharShift = j - badChar[text.charCodeAt(s + j)];
            s += Math.max(1, badCharShift);
        }
    }
    
    return matches;
}

function buildBadCharTable(pattern) {
    const m = pattern.length;
    const NO_OF_CHARS = 256;
    const badChar = new Array(NO_OF_CHARS).fill(-1);
    
    // Fill the actual value of last occurrence
    for (let i = 0; i < m; i++) {
        badChar[pattern.charCodeAt(i)] = i;
    }
    
    return badChar;
}

console.log(boyerMooreSearch("ABAAABCD", "ABC"));
// Output: [4]

console.log(boyerMooreSearch("AABAACAADAABAABA", "AABA"));
// Output: [0, 9, 12]

// Best case: O(n/m) when pattern never matches
// Example: Pattern "ABCD" in "ZZZZZZZZ"
//          Can skip m characters at each mismatch
Note: Boyer-Moore is often the fastest practical algorithm for searching text, especially with large alphabets (like English). The bad character heuristic allows skipping multiple characters when a mismatch occurs.
Component Description Purpose
Trie Build trie of all patterns Efficient prefix matching
Failure Links Link to longest proper suffix that's also in trie Navigate on mismatch like KMP
Output Links Link to other patterns that end at current position Report all matching patterns
Preprocessing O(∑m) where ∑m is total pattern length Build trie + compute failure links
Matching O(n + k) where k is number of matches Linear in text length + output
Applications Virus scanning, spam filtering, DNA analysis Search for thousands of patterns simultaneously

Example: Aho-Corasick Algorithm

class AhoCorasick {
    constructor(patterns) {
        this.trie = {children: {}, output: [], fail: null};
        this.buildTrie(patterns);
        this.buildFailureLinks();
    }
    
    buildTrie(patterns) {
        // Insert all patterns into trie
        for (let i = 0; i < patterns.length; i++) {
            let node = this.trie;
            
            for (const char of patterns[i]) {
                if (!node.children[char]) {
                    node.children[char] = {
                        children: {},
                        output: [],
                        fail: null
                    };
                }
                node = node.children[char];
            }
            
            // Mark end of pattern
            node.output.push(i);
        }
    }
    
    buildFailureLinks() {
        const queue = [];
        
        // Root failure link points to itself
        this.trie.fail = this.trie;
        
        // First level failure links point to root
        for (const char in this.trie.children) {
            const child = this.trie.children[char];
            child.fail = this.trie;
            queue.push(child);
        }
        
        // BFS to build failure links
        while (queue.length > 0) {
            const current = queue.shift();
            
            for (const char in current.children) {
                const child = current.children[char];
                queue.push(child);
                
                // Find failure link
                let failNode = current.fail;
                while (failNode !== this.trie && !failNode.children[char]) {
                    failNode = failNode.fail;
                }
                
                child.fail = failNode.children[char] || this.trie;
                
                // Inherit outputs from failure link
                child.output = child.output.concat(child.fail.output);
            }
        }
    }
    
    search(text) {
        const results = [];
        let node = this.trie;
        
        for (let i = 0; i < text.length; i++) {
            const char = text[i];
            
            // Follow failure links until we find a match
            while (node !== this.trie && !node.children[char]) {
                node = node.fail;
            }
            
            node = node.children[char] || this.trie;
            
            // Report all patterns ending at this position
            for (const patternIndex of node.output) {
                results.push({
                    pattern: patternIndex,
                    position: i
                });
            }
        }
        
        return results;
    }
}

// Example usage
const patterns = ["he", "she", "his", "hers"];
const ac = new AhoCorasick(patterns);
const text = "ushers";

const matches = ac.search(text);
console.log("Patterns:", patterns);
console.log("Text:", text);
console.log("Matches:", matches);
// Output: [
//   {pattern: 1, position: 2}, // "she" at position 0-2
//   {pattern: 0, position: 3}, // "he" at position 2-3
//   {pattern: 3, position: 5}  // "hers" at position 2-5
// ]

// Time: O(n + k) where k is number of matches
// Can find ALL patterns in single pass!

7. Manacher Longest Palindromic Substring

Aspect Description Innovation
Problem Find longest palindromic substring in O(n) Naive approach is O(n²) or O(n²) with DP
Preprocessing Insert '#' between characters Handles even/odd length palindromes uniformly
P Array P[i] = radius of palindrome centered at i Reuse previously computed information
Mirror Property Use symmetry of palindromes to avoid recomputation Core insight for linear time
Time Complexity O(n) Each position expanded at most once
Applications DNA analysis, pattern recognition, compression Finding all palindromes in linear time

Example: Manacher's Algorithm

function manacher(s) {
    // Preprocess: insert '#' between characters
    // "abc" becomes "#a#b#c#"
    const t = '#' + s.split('').join('#') + '#';
    const n = t.length;
    const p = new Array(n).fill(0); // Palindrome radius array
    
    let center = 0; // Center of rightmost palindrome
    let right = 0;  // Right boundary of rightmost palindrome
    
    for (let i = 0; i < n; i++) {
        // Mirror of i with respect to center
        const mirror = 2 * center - i;
        
        // If i is within right boundary, use mirror
        if (i < right) {
            p[i] = Math.min(right - i, p[mirror]);
        }
        
        // Expand around center i
        while (i + p[i] + 1 < n && i - p[i] - 1 >= 0 &&
               t[i + p[i] + 1] === t[i - p[i] - 1]) {
            p[i]++;
        }
        
        // Update center and right if palindrome extends beyond right
        if (i + p[i] > right) {
            center = i;
            right = i + p[i];
        }
    }
    
    // Find maximum palindrome
    let maxLen = 0;
    let centerIndex = 0;
    
    for (let i = 0; i < n; i++) {
        if (p[i] > maxLen) {
            maxLen = p[i];
            centerIndex = i;
        }
    }
    
    // Extract palindrome from original string
    const start = Math.floor((centerIndex - maxLen) / 2);
    return s.substring(start, start + maxLen);
}

console.log(manacher("babad"));      // Output: "bab" or "aba"
console.log(manacher("cbbd"));       // Output: "bb"
console.log(manacher("racecar"));    // Output: "racecar"
console.log(manacher("bananas"));    // Output: "anana"

// Time: O(n), Space: O(n)

Example: Find All Palindromes with Manacher

function findAllPalindromes(s) {
    const t = '#' + s.split('').join('#') + '#';
    const n = t.length;
    const p = new Array(n).fill(0);
    const palindromes = [];
    
    let center = 0, right = 0;
    
    // Build P array using Manacher's algorithm
    for (let i = 0; i < n; i++) {
        const mirror = 2 * center - i;
        if (i < right) p[i] = Math.min(right - i, p[mirror]);
        
        while (i + p[i] + 1 < n && i - p[i] - 1 >= 0 &&
               t[i + p[i] + 1] === t[i - p[i] - 1]) {
            p[i]++;
        }
        
        if (i + p[i] > right) {
            center = i;
            right = i + p[i];
        }
    }
    
    // Extract all palindromes
    for (let i = 0; i < n; i++) {
        if (p[i] > 0) {
            const start = Math.floor((i - p[i]) / 2);
            const length = p[i];
            if (length > 0) {
                const palindrome = s.substring(start, start + length);
                palindromes.push({
                    text: palindrome,
                    start: start,
                    length: length
                });
            }
        }
    }
    
    return palindromes;
}

const allPalindromes = findAllPalindromes("abacabad");
console.log(allPalindromes);
// Output includes: "aba", "aca", "aba", "abacaba", etc.
// All palindromes found in O(n) time!

8. Suffix Array Construction

Concept Description Use Case
Suffix Array Sorted array of all suffixes of string Compressed alternative to suffix tree
Construction Naive: O(n² log n), Optimized: O(n log n) Various algorithms: prefix doubling, DC3
Pattern Search Binary search on suffix array: O(m log n) Multiple searches after one-time construction
LCP Array Longest Common Prefix between consecutive suffixes Finding repeated substrings, compression
Space O(n) More memory-efficient than suffix tree
Applications Text compression, genome assembly, data compression Burrows-Wheeler transform, FM-index

Example: Suffix Array (Simple Construction)

function buildSuffixArray(text) {
    const n = text.length;
    const suffixes = [];
    
    // Create array of (suffix, original index) pairs
    for (let i = 0; i < n; i++) {
        suffixes.push({
            index: i,
            suffix: text.substring(i)
        });
    }
    
    // Sort suffixes lexicographically
    suffixes.sort((a, b) => a.suffix.localeCompare(b.suffix));
    
    // Extract indices
    return suffixes.map(s => s.index);
}

const text = "banana";
const sa = buildSuffixArray(text);

console.log("Text:", text);
console.log("Suffix Array:", sa);
// Output: [5, 3, 1, 0, 4, 2]
// Meaning:
// sa[0] = 5: "a" (banana[5:])
// sa[1] = 3: "ana" (banana[3:])
// sa[2] = 1: "anana" (banana[1:])
// sa[3] = 0: "banana" (banana[0:])
// sa[4] = 4: "na" (banana[4:])
// sa[5] = 2: "nana" (banana[2:])

console.log("\nSorted Suffixes:");
sa.forEach((index, i) => {
    console.log(`${i}: ${text.substring(index)}`);
});

// Time: O(n² log n) for naive approach
// Space: O(n)

Example: Pattern Search using Suffix Array

function searchPattern(text, pattern, suffixArray) {
    const n = text.length;
    const m = pattern.length;
    
    // Binary search for leftmost occurrence
    let left = 0, right = n - 1;
    let leftBound = -1;
    
    while (left <= right) {
        const mid = Math.floor((left + right) / 2);
        const suffix = text.substring(suffixArray[mid]);
        
        if (suffix.substring(0, m) >= pattern) {
            right = mid - 1;
            if (suffix.substring(0, m) === pattern) {
                leftBound = mid;
            }
        } else {
            left = mid + 1;
        }
    }
    
    if (leftBound === -1) return []; // Pattern not found
    
    // Binary search for rightmost occurrence
    left = 0;
    right = n - 1;
    let rightBound = -1;
    
    while (left <= right) {
        const mid = Math.floor((left + right) / 2);
        const suffix = text.substring(suffixArray[mid]);
        
        if (suffix.substring(0, m) <= pattern) {
            left = mid + 1;
            if (suffix.substring(0, m) === pattern) {
                rightBound = mid;
            }
        } else {
            right = mid - 1;
        }
    }
    
    // All occurrences are in range [leftBound, rightBound]
    const positions = [];
    for (let i = leftBound; i <= rightBound; i++) {
        positions.push(suffixArray[i]);
    }
    
    return positions.sort((a, b) => a - b);
}

const text2 = "abracadabra";
const sa2 = buildSuffixArray(text2);

console.log(searchPattern(text2, "abra", sa2));
// Output: [0, 7] - "abra" found at positions 0 and 7

console.log(searchPattern(text2, "cad", sa2));
// Output: [4] - "cad" found at position 4

// Search Time: O(m log n) for pattern of length m

9. Edit Distance Levenshtein DP

Operation Description Cost
Insert Add a character 1 (typically)
Delete Remove a character 1 (typically)
Replace Change one character to another 1 or 2 (Levenshtein vs Damerau)
Algorithm Dynamic programming with 2D table dp[i][j] = min operations for s1[0..i] to s2[0..j]
Time Complexity O(m × n) Fill m×n table
Space Complexity O(m × n) or O(min(m,n)) optimized Can use rolling array for space optimization
Applications Spell checking, DNA sequencing, diff tools, autocorrect Measuring string similarity

Example: Edit Distance (Levenshtein)

function editDistance(str1, str2) {
    const m = str1.length;
    const n = str2.length;
    
    // Create DP table
    const dp = Array(m + 1).fill(null)
                           .map(() => Array(n + 1).fill(0));
    
    // Base cases: empty string conversions
    for (let i = 0; i <= m; i++) dp[i][0] = i; // Delete all
    for (let j = 0; j <= n; j++) dp[0][j] = j; // Insert all
    
    // Fill DP table
    for (let i = 1; i <= m; i++) {
        for (let j = 1; j <= n; j++) {
            if (str1[i - 1] === str2[j - 1]) {
                // Characters match, no operation needed
                dp[i][j] = dp[i - 1][j - 1];
            } else {
                // Take minimum of insert, delete, replace
                dp[i][j] = 1 + Math.min(
                    dp[i - 1][j],     // Delete from str1
                    dp[i][j - 1],     // Insert into str1
                    dp[i - 1][j - 1]  // Replace in str1
                );
            }
        }
    }
    
    return dp[m][n];
}

console.log(editDistance("kitten", "sitting"));
// Output: 3 (kitten → sitten → sittin → sitting)

console.log(editDistance("sunday", "saturday"));
// Output: 3 (sunday → sturday → saturay → saturday)

console.log(editDistance("horse", "ros"));
// Output: 3 (horse → rorse → rose → ros)

// Time: O(m × n), Space: O(m × n)

Example: Edit Distance with Path Reconstruction

function editDistanceWithPath(str1, str2) {
    const m = str1.length;
    const n = str2.length;
    const dp = Array(m + 1).fill(null).map(() => Array(n + 1).fill(0));
    
    for (let i = 0; i <= m; i++) dp[i][0] = i;
    for (let j = 0; j <= n; j++) dp[0][j] = j;
    
    for (let i = 1; i <= m; i++) {
        for (let j = 1; j <= n; j++) {
            if (str1[i - 1] === str2[j - 1]) {
                dp[i][j] = dp[i - 1][j - 1];
            } else {
                dp[i][j] = 1 + Math.min(
                    dp[i - 1][j],
                    dp[i][j - 1],
                    dp[i - 1][j - 1]
                );
            }
        }
    }
    
    // Reconstruct path
    const operations = [];
    let i = m, j = n;
    
    while (i > 0 || j > 0) {
        if (i === 0) {
            operations.push(`Insert '${str2[j - 1]}'`);
            j--;
        } else if (j === 0) {
            operations.push(`Delete '${str1[i - 1]}'`);
            i--;
        } else if (str1[i - 1] === str2[j - 1]) {
            operations.push(`Keep '${str1[i - 1]}'`);
            i--;
            j--;
        } else {
            const deleteCost = dp[i - 1][j];
            const insertCost = dp[i][j - 1];
            const replaceCost = dp[i - 1][j - 1];
            
            if (replaceCost <= deleteCost && replaceCost <= insertCost) {
                operations.push(`Replace '${str1[i - 1]}' with '${str2[j - 1]}'`);
                i--;
                j--;
            } else if (deleteCost <= insertCost) {
                operations.push(`Delete '${str1[i - 1]}'`);
                i--;
            } else {
                operations.push(`Insert '${str2[j - 1]}'`);
                j--;
            }
        }
    }
    
    return {
        distance: dp[m][n],
        operations: operations.reverse()
    };
}

const result = editDistanceWithPath("kitten", "sitting");
console.log("Distance:", result.distance);
console.log("Operations:", result.operations);
// Shows exact sequence of operations to transform string

10. Trie-based String Operations

Operation Time Complexity Description
Insert O(m) Add word of length m to trie
Search O(m) Check if word exists
Prefix Search O(m) Find all words with given prefix
Delete O(m) Remove word from trie
Autocomplete O(m + k) Find k suggestions for prefix m
Space O(ALPHABET_SIZE × N × M) N words, M avg length, can be optimized

Example: Trie with Autocomplete

class TrieNode {
    constructor() {
        this.children = {};
        this.isEndOfWord = false;
        this.frequency = 0; // For ranking suggestions
    }
}

class Trie {
    constructor() {
        this.root = new TrieNode();
    }
    
    insert(word, frequency = 1) {
        let node = this.root;
        
        for (const char of word) {
            if (!node.children[char]) {
                node.children[char] = new TrieNode();
            }
            node = node.children[char];
        }
        
        node.isEndOfWord = true;
        node.frequency = frequency;
    }
    
    search(word) {
        let node = this.root;
        
        for (const char of word) {
            if (!node.children[char]) {
                return false;
            }
            node = node.children[char];
        }
        
        return node.isEndOfWord;
    }
    
    startsWith(prefix) {
        let node = this.root;
        
        for (const char of prefix) {
            if (!node.children[char]) {
                return false;
            }
            node = node.children[char];
        }
        
        return true;
    }
    
    autocomplete(prefix, limit = 5) {
        let node = this.root;
        
        // Navigate to prefix
        for (const char of prefix) {
            if (!node.children[char]) {
                return [];
            }
            node = node.children[char];
        }
        
        // DFS to find all words with this prefix
        const suggestions = [];
        this.dfs(node, prefix, suggestions);
        
        // Sort by frequency and return top results
        return suggestions
            .sort((a, b) => b.frequency - a.frequency)
            .slice(0, limit)
            .map(s => s.word);
    }
    
    dfs(node, currentWord, suggestions) {
        if (node.isEndOfWord) {
            suggestions.push({
                word: currentWord,
                frequency: node.frequency
            });
        }
        
        for (const char in node.children) {
            this.dfs(node.children[char], currentWord + char, suggestions);
        }
    }
    
    delete(word) {
        this.deleteHelper(this.root, word, 0);
    }
    
    deleteHelper(node, word, index) {
        if (index === word.length) {
            if (!node.isEndOfWord) return false;
            node.isEndOfWord = false;
            return Object.keys(node.children).length === 0;
        }
        
        const char = word[index];
        if (!node.children[char]) return false;
        
        const shouldDeleteChild = this.deleteHelper(
            node.children[char],
            word,
            index + 1
        );
        
        if (shouldDeleteChild) {
            delete node.children[char];
            return Object.keys(node.children).length === 0 && !node.isEndOfWord;
        }
        
        return false;
    }
}

// Example usage
const trie = new Trie();

// Insert words with frequencies
const words = [
    {word: "apple", freq: 100},
    {word: "app", freq: 80},
    {word: "application", freq: 60},
    {word: "apply", freq: 70},
    {word: "appreciate", freq: 50},
    {word: "apricot", freq: 30}
];

words.forEach(({word, freq}) => trie.insert(word, freq));

console.log(trie.search("apple"));        // true
console.log(trie.search("appl"));         // false
console.log(trie.startsWith("app"));      // true

console.log(trie.autocomplete("app", 3));
// Output: ["apple", "app", "apply"] - Top 3 by frequency

console.log(trie.autocomplete("appr", 5));
// Output: ["appreciate"]

trie.delete("app");
console.log(trie.search("app"));          // false
console.log(trie.search("apple"));        // true (still exists)

String Algorithms Summary

  • Naive: O(n×m), simple but slow, good for teaching/debugging
  • KMP: O(n+m), no backtracking, LPS array, single pattern optimal
  • Rabin-Karp: O(n+m) avg, rolling hash, great for multiple patterns
  • Z Algorithm: O(n+m), Z array, simpler than KMP for some problems
  • Boyer-Moore: O(n/m) best, fastest in practice, used in grep/editors
  • Aho-Corasick: O(n+k), multiple patterns simultaneously, virus scanning
  • Manacher: O(n), longest palindrome, mirror property optimization
  • Suffix Array: O(n log n) build, O(m log n) search, genome analysis
  • Edit Distance: O(m×n), DP solution, spell check, DNA alignment
  • Trie: O(m) operations, prefix matching, autocomplete, dictionaries