String matching: KMP and Rabin-Karp
Find every occurrence of a pattern in a text: why brute force can be O(n·m), how KMP never re-reads a letter using its failure table, how Rabin-Karp compares rolling hashes, and what .NET does for you.
AAAA…A, pattern AAAB) every alignment almost matches, and the cost is O(n·m).
Console.WriteLine(string.Join(", ", FindAll("AABAACAADAABAABA", "AABA"))); static List<int> FindAll(string text, string pattern){ var found = new List<int>(); for (int s = 0; s + pattern.Length <= text.Length; s++) // every alignment { int j = 0; while (j < pattern.Length && text[s + j] == pattern[j]) j++; if (j == pattern.Length) found.Add(s); } return found;}0, 9, 12
AAAAAAAAAAAAAAAB with pattern AAAB, to see brute force at its worst.
KMP: never look back
| Pattern | A | B | A | B | C | A | B | A | B |
|---|---|---|---|---|---|---|---|---|---|
ABAB (LPS 2), its last two letters AB are also its first two. If the next letter fails, the pattern can slide so those two letters line up, without comparing them again.
Console.WriteLine(string.Join(" ", BuildLps("ABABCABAB")));Console.WriteLine(string.Join(", ", Search("ABABDABACDABABCABAB", "ABABCABAB")));Console.WriteLine(string.Join(", ", Search("AABAACAADAABAABA", "AABA"))); static int[] BuildLps(string p){ var lps = new int[p.Length]; int len = 0; // length of the current prefix that is also a suffix for (int i = 1; i < p.Length; ) { if (p[i] == p[len]) lps[i++] = ++len; // extend it else if (len > 0) len = lps[len - 1]; // fall back to a shorter one else lps[i++] = 0; } return lps;} static List<int> Search(string text, string p){ var lps = BuildLps(p); var found = new List<int>(); for (int i = 0, j = 0; i < text.Length; ) { if (text[i] == p[j]) { i++; j++; if (j == p.Length) { found.Add(i - j); j = lps[j - 1]; } // keep the overlap } else if (j > 0) j = lps[j - 1]; // slide the pattern; i stays where it is else i++; } return found;}0 0 1 2 0 1 2 3 4 10 0, 9, 12
i only ever moves forward, and every fallback of j is paid for by an earlier step forward, so the search is O(n) and building the table is O(m): O(n + m) in total, even in the worst case.
Rabin-Karp: compare numbers, not letters
Console.WriteLine(string.Join(", ", RabinKarp("AABAACAADAABAABA", "AABA"))); static List<int> RabinKarp(string text, string p){ const long B = 256, M = 1_000_000_007; int m = p.Length; var found = new List<int>(); if (m > text.Length) return found; long high = 1; // B^(m-1) mod M: the weight of the leftmost letter for (int i = 1; i < m; i++) high = high * B % M; long ph = 0, wh = 0; for (int i = 0; i < m; i++) { ph = (ph * B + p[i]) % M; wh = (wh * B + text[i]) % M; } for (int s = 0; ; s++) { if (wh == ph && string.CompareOrdinal(text, s, p, 0, m) == 0) found.Add(s); // same hash: check letters if (s + m == text.Length) break; wh = (wh - text[s] * high % M + M) % M; // drop the left letter wh = (wh * B + text[s + m]) % M; // bring in the right letter } return found;}0, 9, 12
In real .NET code
string.IndexOf, Contains and Span.IndexOf are heavily optimised with vector (SIMD) instructions and scan many characters per CPU step. One thing matters a lot, though: pass `StringComparison.Ordinal` (or OrdinalIgnoreCase) when you mean exact characters. IndexOf(string) without it uses the current culture’s rules, which is slower and can give surprising results.
string log = "ERR;OK;ERR;ERR;OK;ERRATA";int count = 0;for (int i = log.IndexOf("ERR", StringComparison.Ordinal); i >= 0; i = log.IndexOf("ERR", i + 1, StringComparison.Ordinal)) count++;Console.WriteLine(count);Console.WriteLine(log.Contains("ok", StringComparison.OrdinalIgnoreCase));4 True
| Algorithm | Time | Best for |
|---|---|---|
ABCABCABC → ABC, ABAB → AB, ABCAB → ABCAB (no repetition), AAAA → A.
Show a hint
Show the solution
foreach (var s in new[] { "ABCABCABC", "ABAB", "ABCAB", "AAAA" }) Console.WriteLine($"{s}: {Unit(s)}"); static string Unit(string s){ var lps = new int[s.Length]; for (int i = 1, len = 0; i < s.Length; ) { if (s[i] == s[len]) lps[i++] = ++len; else if (len > 0) len = lps[len - 1]; else lps[i++] = 0; } int period = s.Length - lps[^1]; return s.Length % period == 0 ? s[..period] : s;}ABCABCABC: ABC ABAB: AB ABCAB: ABCAB AAAA: A
Remember
Brute-force matching is O(n·m) in the worst case because it re-reads text after every mismatch. KMP precomputes the longest prefix-that-is-also-a-suffix table and never moves backwards in the text: O(n + m). Rabin-Karp compares rolling hashes in O(1) per window and confirms hash matches letter by letter. In .NET, use IndexOf / Contains with StringComparison.Ordinal; they are vectorised and fast.