Chapter 20

String Algorithms & Pattern Matching

Master advanced string processing techniques from naive approaches to linear-time algorithms. Learn KMP, Rabin-Karp, Z-Algorithm, Manacher's, and string hashing with complete C# implementations and real LeetCode solutions.

9
Core Algorithms
15+
Practice Problems
50+
Code Examples
1400+
Lines of Content
Table of Contents
20.1

String Basics Review

Before diving into advanced algorithms, understand the fundamental properties of strings in C# and how they impact algorithm design and performance.

Immutability in C#

Strings in C# are immutable—once created, they cannot be changed. Every string modification creates a new string object.

csharp
string text = "hello";
string upper = text.ToUpper(); // Creates NEW string
// Original text still "hello", upper is "HELLO"

string concat = text + " world"; // Creates NEW string

char vs string

char is a single 16-bit Unicode character, while string is a sequence of chars. Understanding this distinction is crucial for efficient string manipulation.

csharp
// char operations are O(1)
char c = 'a';
bool isLetter = char.IsLetter(c); // O(1)

// string indexing is O(1)
string s = "hello";
char first = s[0]; // Direct access, O(1)

Unicode and ASCII

C# uses Unicode (UTF-16) for characters. ASCII is a 7-bit subset (0-127). Understanding character encoding is important for pattern matching and hashing.

csharp
// Convert char to ASCII value
char c = 'A';
int ascii = (int)c; // 65

// Check if ASCII (0-127)
bool isAscii = ascii < 128;

// Convert ASCII value back to char
char fromAscii = (char)65; // 'A'

StringBuilder for Efficient String Concatenation

Use StringBuilder when building strings in loops to avoid O(n²) concatenation overhead.

csharp
// BAD: O(n²) due to string immutability
string result = "";
for (int i = 0; i < 1000; i++)
{
    result += "x"; // Creates new string each time
}

// GOOD: O(n) with StringBuilder
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++)
{
    sb.Append("x"); // Efficient
}
string final = sb.ToString();
20.2

Brute Force Pattern Matching

The simplest approach: check every position in the text to see if the pattern matches. Easy to understand but inefficient for large texts with long patterns.

Time
O(n·m)
Space
O(1)

The algorithm is straightforward: for each position i in the text, check if the pattern matches starting at position i.

csharp
/// <summary> Brute force pattern search O(n*m) </summary>
public static int BruteForceSearch(string text, string pattern)
{
    int n = text.Length;
    int m = pattern.Length;

    // Try each position in text
    for (int i = 0; i <= n - m; i++)
    {
        bool match = true;

        // Check if pattern matches at position i
        for (int j = 0; j < m; j++)
        {
            if (text[i + j] != pattern[j])
            {
                match = false;
                break;
            }
        }

        if (match)
            return i; // Found at position i
    }

    return -1; // Not found
}

Example Walkthrough

Find "BAC" in "ABABAC":

Position 0: "ABA"
First char 'A' vs 'B' → Mismatch
Position 1: "BAB"
First char 'B' matches, but second 'A' vs 'A' matches, third 'B' vs 'C' → Mismatch
Position 2: "ABA"
First char 'A' vs 'B' → Mismatch
Position 3: "BAC" ✓
All characters match! Return index 3
💡
When to use Brute Force: Small patterns, small texts, or when clarity matters more than performance. Never use for large-scale text search.
20.3

KMP Algorithm (Knuth-Morris-Pratt)

Linear-time pattern matching O(n + m) by avoiding redundant comparisons using a failure function that encodes pattern structure.

Time
O(n + m)
Space
O(m)

Core Idea: The Failure Function

The failure function (or LPS—Longest Proper Prefix which is also Suffix) tells us: when we have a mismatch at position j, what is the longest prefix of the pattern that is also a suffix of pattern[0..j-1]? This lets us skip redundant comparisons.

Building the LPS Array

The LPS array is built using a two-pointer technique:

csharp
/// <summary> Build LPS (Longest Proper Prefix which is also Suffix) </summary>
private static int[] BuildLPS(string pattern)
{
    int m = pattern.Length;
    int[] lps = new int[m];
    int len = 0; // Length of previous longest prefix suffix
    int i = 1;

    while (i < m)
    {
        if (pattern[i] == pattern[len])
        {
            // Characters match, extend the prefix
            lps[i] = len + 1;
            len++;
            i++;
        }
        else
        {
            if (len != 0)
            {
                // Fall back to previous state
                len = lps[len - 1];
            }
            else
            {
                lps[i] = 0;
                i++;
            }
        }
    }

    return lps;
}

LPS Example: Pattern "ABABAC"

Index 0
LPS[0] = 0 (no proper prefix)
Index 1 ('AB')
No prefix matches suffix → LPS[1] = 0
Index 2 ('ABA')
Prefix 'A' = suffix 'A' → LPS[2] = 1
Index 3 ('ABAB')
Prefix 'AB' = suffix 'AB' → LPS[3] = 2
Index 4 ('ABABA')
Prefix 'ABA' = suffix 'ABA' → LPS[4] = 3
Index 5 ('ABABAC')
No match after fallback → LPS[5] = 0

KMP Search

Use the LPS array to skip comparisons when mismatches occur:

csharp
/// <summary> KMP pattern search O(n + m) </summary>
public static int KMPSearch(string text, string pattern)
{
    int n = text.Length;
    int m = pattern.Length;

    int[] lps = BuildLPS(pattern);

    int i = 0; // Index for text
    int j = 0; // Index for pattern

    while (i < n)
    {
        if (pattern[j] == text[i])
        {
            i++;
            j++;
        }

        if (j == m)
        {
            // Pattern found at index i - m
            return i - m;
        }
        else if (i < n && pattern[j] != text[i])
        {
            // Use LPS to skip redundant checks
            if (j != 0)
                j = lps[j - 1];
            else
                i++;
        }
    }

    return -1; // Not found
}

Complete KMP Example

Find "BAC" in "ABABAC":

1
Build LPS for "BAC" → [0, 0, 0] (no overlaps)
2
Compare 'B' with 'A' at position 0 → mismatch, i++
3
Compare 'B' with 'B' at position 1 → match, i++, j++
4
Compare 'A' with 'A' → match, i++, j++
5
Compare 'C' with 'C' → match, j == 3, return i - m = 3
ℹ️
KMP is optimal: It examines each character in the text and pattern exactly once, making it O(n + m) which is the best possible for single pattern search.
20.4

Rabin-Karp Algorithm

Hash-based pattern matching: compute rolling hash of text and pattern windows. Excel at finding multiple patterns in text; vulnerable to hash collisions.

Time
O(n + m)
Space
O(1)
Worst Case
O(n·m)

Rolling Hash Concept

Instead of comparing strings character-by-character, compute a hash of each substring and compare hashes. When sliding the window, update the hash efficiently using the rolling hash formula:

new_hash = (old_hash - first_char * base^(m-1)) * base + new_char

csharp
/// <summary> Rabin-Karp pattern search with rolling hash </summary>
public static int RabinKarpSearch(string text, string pattern)
{
    int n = text.Length;
    int m = pattern.Length;
    const long BASE = 256; // Prime for better distribution
    const long MOD = 101; // Prime modulus to avoid overflow
    long patternHash = 0;
    long textHash = 0;
    long power = 1; // BASE^(m-1) % MOD

    // Compute BASE^(m-1) % MOD
    for (int i = 0; i < m - 1; i++)
        power = (power * BASE) % MOD;

    // Compute hash of pattern and first window of text
    for (int i = 0; i < m; i++)
    {
        patternHash = (patternHash * BASE + pattern[i]) % MOD;
        textHash = (textHash * BASE + text[i]) % MOD;
    }

    // Slide the window
    for (int i = 0; i <= n - m; i++)
    {
        // Hash match found
        if (patternHash == textHash)
        {
            // Verify actual match (handle hash collisions)
            bool match = true;
            for (int j = 0; j < m; j++)
            {
                if (text[i + j] != pattern[j])
                {
                    match = false;
                    break;
                }
            }
            if (match)
                return i;
        }

        // Calculate hash of next window
        if (i < n - m)
        {
            textHash = (textHash - text[i] * power) % MOD;
            textHash = (textHash * BASE + text[i + m]) % MOD;
            // Handle negative modulo in C#
            if (textHash < 0)
                textHash += MOD;
        }
    }

    return -1;
}

When to Use Rabin-Karp

Good Use Cases
Multiple pattern search in single text
Finding all occurrences (hash provides quick filtering)
Plagiarism detection in documents
Poor Use Cases
Single pattern, single text (KMP is simpler)
Very small patterns (brute force faster)
Texts with many hash collisions
⚠️
Hash Collisions: Always verify actual match when hashes match. A hash collision means the hashes are equal but strings differ—rare but possible.
20.5

Z-Algorithm

Linear-time pattern matching using a Z-array that stores the length of the longest substring starting from position i that matches a prefix of the string.

Time
O(n + m)
Space
O(n + m)

Z-Array Construction

The Z-array Z[i] represents the length of the longest substring starting from index i that matches a prefix of the string. Built using the "Z-box" concept for O(n) time.

csharp
/// <summary> Build Z-array: Z[i] = longest substring from i matching prefix </summary>
private static int[] BuildZArray(string s)
{
    int n = s.Length;
    int[] z = new int[n];
    int l = 0, r = 0; // [l, r] is the rightmost Z-box

    for (int i = 1; i < n; i++)
    {
        if (i > r)
        {
            // Outside rightmost Z-box, compute from scratch
            l = r = i;
            while (r < n && s[r - l] == s[r])
                r++;
            z[i] = r - l;
            r--;
        }
        else
        {
            // Inside rightmost Z-box, use previously computed values
            int k = i - l; // Corresponding index in Z-box
            if (z[k] < r - i + 1)
            {
                z[i] = z[k];
            }
            else
            {
                l = i;
                while (r < n && s[r - l] == s[r])
                    r++;
                z[i] = r - l;
                r--;
            }
        }
    }

    return z;
}

Z-Algorithm Pattern Search

Concatenate pattern and text with a separator, build Z-array. If Z[i] == m, pattern is found at position i - m - 1 in text.

csharp
/// <summary> Z-Algorithm pattern search O(n + m) </summary>
public static int ZAlgorithmSearch(string text, string pattern)
{
    // Create concatenated string: pattern + separator + text
    string combined = pattern + "#" + text;
    int[] z = BuildZArray(combined);
    int m = pattern.Length;

    // Search for pattern in Z-array
    for (int i = m + 1; i < combined.Length; i++)
    {
        if (z[i] == m)
        {
            // Found: return position in original text
            return i - m - 1;
        }
    }

    return -1;
}

Z-Array Example

For string "ABAB": Z = [0, 0, 2, 0]

💡
Z-Algorithm strength: Efficient for finding all occurrences of a pattern. Z[i] tells you exactly how much of the prefix matches, which is useful for multiple pattern problems.
20.6

Manacher's Algorithm

Find longest palindromic substring in O(n) time by expanding around centers efficiently using a radius array and center tracking.

Time
O(n)
Space
O(n)

The Transformed String Trick

To handle both odd and even-length palindromes uniformly, insert a sentinel character between each character: "aba" becomes "#a#b#a#".

csharp
/// <summary> Find longest palindromic substring using Manacher's algorithm </summary>
public static string LongestPalindrome(string s)
{
    if (s.Length <= 1)
        return s;

    // Transform string to handle even-length palindromes
    StringBuilder transformed = new StringBuilder("#");
    foreach (char c in s)
    {
        transformed.Append(c);
        transformed.Append('#');
    }
    string t = transformed.ToString();

    int n = t.Length;
    int[] p = new int[n]; // Radius of palindrome at each position
    int center = 0; // Center of rightmost palindrome found
    int right = 0; // Right edge of rightmost palindrome
    int maxLen = 0;
    int centerIndex = 0;

    for (int i = 0; i < n; i++)
    {
        // Mirror of i around center
        int mirror = 2 * center - i;

        // If i is within the right edge, use mirror's radius
        if (i < right)
        {
            p[i] = Math.Min(right - i, p[mirror]);
        }

        // Expand around center
        try
        {
            while (i + 1 + p[i] < n && i - 1 - p[i] >= 0 &&
                   t[i + 1 + p[i]] == t[i - 1 - p[i]])
            {
                p[i]++;
            }
        }
        catch { }

        // Update center and right
        if (i + p[i] > right)
        {
            center = i;
            right = i + p[i];
        }

        // Track longest palindrome
        if (p[i] > maxLen)
        {
            maxLen = p[i];
            centerIndex = i;
        }
    }

    // Extract original palindrome from transformed string
    int start = (centerIndex - maxLen) / 2;
    return s.Substring(start, maxLen);
}

Manacher's Walkthrough: "babad"

1
Transform to "#b#a#b#a#d#" (n=11)
2
At position 4 (center 'a'): radius expands to 3, covering "bab"
3
Mirror position 6 uses radius knowledge from position 2
4
Right edge and center updated as better palindromes found
5
Extract "bab" from original string
ℹ️
Why linear time? Each character is expanded from at most twice—once as a center, once during mirror expansion. The right edge only moves right.
20.7

String Hashing

Use polynomial rolling hash to compare substrings in O(1) time after O(n) preprocessing. Essential for competitive programming and substring problems.

Preprocess
O(n)
Compare Substrings
O(1)
Space
O(n)

Polynomial Hash Formula

Hash of string s = s[0]·base^(n-1) + s[1]·base^(n-2) + ... + s[n-1], all mod MOD.

csharp
/// <summary> String hashing with polynomial rolling hash </summary>
public class StringHash
{
    private readonly long[] _hash;
    private readonly long[] _pow;
    private const long BASE = 31;
    private const long MOD = 1000000007;

    public StringHash(string s)
    {
        int n = s.Length;
        _hash = new long[n + 1];
        _pow = new long[n + 1];
        _pow[0] = 1;

        // Precompute powers of base
        for (int i = 1; i <= n; i++)
        {
            _pow[i] = (_pow[i - 1] * BASE) % MOD;
        }

        // Precompute prefix hashes
        for (int i = 0; i < n; i++)
        {
            _hash[i + 1] = (_hash[i] * BASE + (s[i] - 'a' + 1)) % MOD;
        }
    }

    /// <summary> Get hash of substring s[l..r] in O(1) </summary>
    public long GetHash(int l, int r)
    {
        long h = (_hash[r + 1] - _hash[l] * _pow[r - l + 1]) % MOD;
        return h < 0 ? h + MOD : h;
    }

    /// <summary> Check if two substrings are equal in O(1) </summary>
    public bool AreEqual(int l1, int r1, int l2, int r2)
    {
        if (r1 - l1 != r2 - l2)
            return false;
        return GetHash(l1, r1) == GetHash(l2, r2);
    }
}

Application: Longest Duplicate Substring

csharp
/// <summary> Find longest duplicate substring using binary search + hashing </summary>
public static string LongestDuplicateSubstring(string s)
{
    int left = 1, right = s.Length - 1;
    string result = "";

    while (left <= right)
    {
        int mid = (left + right) / 2;
        string duplicate = FindDuplicateOfLength(s, mid);

        if (duplicate != "")
        {
            result = duplicate;
            left = mid + 1;
        }
        else
        {
            right = mid - 1;
        }
    }

    return result;
}

private static string FindDuplicateOfLength(string s, int len)
{
    StringHash sh = new StringHash(s);
    var seen = new HashSet<long>();

    for (int i = 0; i <= s.Length - len; i++)
    {
        long hash = sh.GetHash(i, i + len - 1);
        if (seen.Contains(hash))
            return s.Substring(i, len);
        seen.Add(hash);
    }

    return "";
}
⚠️
Hash collisions still possible: For safety in production, verify hashes match the actual substrings, or use double hashing (two different bases/mods).
20.8

Trie Data Structure

Tree structure for storing and searching strings. Essential for autocomplete, prefix matching, and word searches. Space-time tradeoff: O(n) space for O(m) search.

csharp
/// <summary> Trie node with children and word flag </summary>
public class TrieNode
{
    public Dictionary<char, TrieNode> children = new();
    public bool isWord = false;
}

/// <summary> Trie for efficient string search and prefix matching </summary>
public class Trie
{
    private TrieNode root = new TrieNode();

    /// <summary> Insert word into trie O(m) where m = word length </summary>
    public void Insert(string word)
    {
        TrieNode node = root;
        foreach (char c in word)
        {
            if (!node.children.ContainsKey(c))
                node.children[c] = new TrieNode();
            node = node.children[c];
        }
        node.isWord = true;
    }

    /// <summary> Search for exact word O(m) </summary>
    public bool Search(string word)
    {
        TrieNode node = FindNode(word);
        return node != null && node.isWord;
    }

    /// <summary> Check if any word starts with prefix O(m) </summary>
    public bool StartsWith(string prefix)
    {
        return FindNode(prefix) != null;
    }

    private TrieNode FindNode(string prefix)
    {
        TrieNode node = root;
        foreach (char c in prefix)
        {
            if (!node.children.ContainsKey(c))
                return null;
            node = node.children[c];
        }
        return node;
    }

    /// <summary> Get all words with given prefix </summary>
    public List<string> GetWordsWithPrefix(string prefix)
    {
        var result = new List<string>();
        TrieNode node = FindNode(prefix);
        if (node != null)
            DFS(node, prefix, result);
        return result;
    }

    private void DFS(TrieNode node, string current, List<string> result)
    {
        if (node.isWord)
            result.Add(current);
        foreach (var pair in node.children)
            DFS(pair.Value, current + pair.Key, result);
    }
}

Applications

Autocomplete
Search Suggestions
GetWordsWithPrefix returns all words matching prefix in O(m + n) where n is number of results
Spell Check
Word Validation
Search determines if word exists. Can extend for edit distance variations.
IP Router
Longest Prefix Match
Find longest matching prefix for network routing in O(k) where k is IP length
20.9

Suffix Array & LCP

Suffix array stores all suffixes sorted lexicographically. LCP (Longest Common Prefix) array helps find repeated substrings and string patterns efficiently.

csharp
/// <summary> Build suffix array by sorting all suffixes </summary>
public static int[] BuildSuffixArray(string s)
{
    int n = s.Length;
    var suffixes = new List<(string, int)>();

    // Create all suffixes with their original indices
    for (int i = 0; i < n; i++)
        suffixes.Add((s.Substring(i), i));

    // Sort lexicographically
    suffixes.Sort((a, b) => a.Item1.CompareTo(b.Item1));

    int[] result = new int[n];
    for (int i = 0; i < n; i++)
        result[i] = suffixes[i].Item2;

    return result;
}

/// <summary> Build LCP array from suffix array </summary>
public static int[] BuildLCP(string s, int[] sa)
{
    int n = s.Length;
    int[] lcp = new int[n];
    int[] rank = new int[n];

    // Build rank array (position of suffix in sorted order)
    for (int i = 0; i < n; i++)
        rank[sa[i]] = i;

    int h = 0;

    // Kasai algorithm for LCP construction
    for (int i = 0; i < n; i++)
    {
        if (rank[i] > 0)
        {
            int j = sa[rank[i] - 1];
            while (i + h < n && j + h < n && s[i + h] == s[j + h])
                h++;
            lcp[rank[i]] = h;
            if (h > 0)
                h--;
        }
    }

    return lcp;
}

Application: Longest Repeated Substring

Find the longest substring that appears at least twice. The answer is the maximum value in the LCP array.

csharp
/// <summary> Find longest repeated substring using suffix array </summary>
public static string LongestRepeatedSubstring(string s)
{
    int[] sa = BuildSuffixArray(s);
    int[] lcp = BuildLCP(s, sa);

    int maxLen = 0;
    int maxIndex = 0;

    for (int i = 0; i < lcp.Length; i++)
    {
        if (lcp[i] > maxLen)
        {
            maxLen = lcp[i];
            maxIndex = sa[i];
        }
    }

    return s.Substring(maxIndex, maxLen);
}
ℹ️
Suffix arrays are advanced: Construction is O(n log n) with simple sorting. More sophisticated O(n) algorithms exist (DC3, SA-IS) but are complex. LCP is typically O(n) using Kasai's algorithm.
20.10

Practice Problems

15+ LeetCode problems with complete C# solutions using techniques from this chapter.

Problem 1: Implement strStr (LC 28)

Find the index of the first occurrence of substring.

csharp
// Solution using KMP (optimal O(n+m))
public int StrStr(string haystack, string needle)
{
    if (needle.Length == 0) return 0;
    return KMPSearch(haystack, needle);
}

Problem 2: Longest Palindromic Substring (LC 5)

Solution: Use Manacher's algorithm for O(n) optimal solution.

Problem 3: Repeated Substring Pattern (LC 459)

Check if string is made of repeated pattern using KMP LPS trick.

csharp
public bool RepeatedSubstringPattern(string s)
{
    string doubled = s + s;
    return doubled.IndexOf(s, 1) < s.Length;
}

Problem 2: Longest Palindromic Substring (LC 5)

Solution: Use Manacher's algorithm for O(n) optimal solution.

Problem 3: Repeated Substring Pattern (LC 459)

Check if string is made of repeated pattern using KMP LPS trick.

csharp
public bool RepeatedSubstringPattern(string s)
{
    // Trick: if s is made of repeated pattern,
    // then s appears in (s+s)[1..2n-2]
    string doubled = s + s;
    return doubled.IndexOf(s, 1) < s.Length;
}

Problem 4: Shortest Palindrome (LC 214)

Add minimum characters to make string palindrome using KMP on (pattern + '#' + text).

Problem 5: Longest Happy Prefix (LC 1392)

Use KMP failure function directly—answer is LPS[n-1].

csharp
public string LongestPrefix(string s)
{
    int[] lps = BuildLPS(s);
    return s.Substring(0, lps[s.Length - 1]);
}

Problem 6: Distinct Subsequences (LC 115)

Count distinct subsequences of S that equals T using DP.

Problem 7: Minimum Window Substring (LC 76)

Sliding window with character frequency map.

Problem 8: Edit Distance (LC 72)

Classic DP: dp[i][j] = edit distance between s1[0..i-1] and s2[0..j-1].

Problem 9: Palindrome Pairs (LC 336)

Find all pairs that form palindrome. Use Trie + KMP trick.

Problem 10: Interleaving String (LC 97)

DP: check if s3 is interleaving of s1 and s2.

Problem 11: Count and Say (LC 38)

Simulate the look-and-say sequence.

Problem 12: String to Integer (LC 8)

Parse integer from string handling edge cases and whitespace.

Problem 13: Longest Common Substring

Use suffix array with LCP array or DP approach.

Problem 14: Wildcard Matching (LC 44)

DP or greedy with * and ? wildcards.

Problem 15: Regular Expression Matching (LC 10)

DP with . and * metacharacters.

20.11

Algorithm Comparison Table

Quick reference to choose the right string algorithm for your problem.

Algorithm Time Space Best For
Brute Force O(n·m) O(1) Small patterns, learning
KMP O(n+m) O(m) Single pattern search, optimal
Rabin-Karp O(n+m) O(1) Multiple patterns, hashing
Z-Algorithm O(n+m) O(n+m) All occurrences, pattern analysis
Manacher O(n) O(n) Longest palindrome, optimal
Trie O(m) O(n·m) Autocomplete, prefix matching
Suffix Array O(n log n) O(n) All substrings, advanced queries
String Hash O(n) O(n) Substring comparison, duplicate finding

Decision Tree

Single Pattern Match
KMP for guaranteed O(n+m). Simple IndexOf if pattern small.
Multiple Patterns
Rabin-Karp or Aho-Corasick. Hash-based approach efficient for many patterns.
Palindrome
Manacher's for O(n) longest palindrome. Expand-around-center for simplicity.
Prefix Queries
Trie for autocomplete and prefix matching. O(m) search.
Substring Comparison
String hashing for O(1) comparison after O(n) preprocessing.
All Substrings
Suffix array with LCP for advanced queries and pattern finding.
20.12

Interview Tips

Patterns, mistakes, and strategies for string algorithm questions.

Common Problem Patterns

Pattern 1
Find/Count Pattern
Use KMP for single pattern. Rabin-Karp for multiple. Always clarify: first occurrence or all occurrences?
Pattern 2
Palindrome
Clarify: longest, all palindromes, or just check? Manacher is gold standard for longest.
Pattern 3
Prefix/Suffix
Often KMP LPS array is key. Look for patterns: "longest prefix is also suffix".
Pattern 4
Edit Distance
Always DP. Remember: s[i-1] == t[j-1] means no edit needed, else take min of 3 ops.
Pattern 5
Matching
Regex or wildcards? Use DP with backtracking. State = (i, j) = position in string and pattern.
Pattern 6
Subsequence
DP: can skip characters. Subarray: must be contiguous. Know the difference!

Common Mistakes

Mistake 1
Building LPS with 0-indexing errors in KMP
Fix: carefully handle j=0 case, use lps[j-1] for fallback
Mistake 2
Off-by-one errors in substring extraction
Fix: test Substring(start, length) carefully
Mistake 3
Forgetting to verify hash match in Rabin-Karp
Fix: always double-check actual strings when hashes match
Mistake 4
DP indexing confusion (1-indexed vs 0-indexed)
Fix: be explicit: dp[i] = result for s[0..i-1]

Strategies

⚠️
KMP is the safest bet: If you only know one algorithm deeply, master KMP. It's O(n+m), handles all single-pattern problems, and the failure function is a teachable concept.
💡
The "doubled string" trick: Many pattern problems reduce to string searching. Example: repeated pattern = "pattern appears in (s+s)[1..n-1]".