<style> .reveal-up, .reveal-left, .reveal-right, .reveal-stagger > * { opacity: 1 !important; transform: none !important; } </style>
  1. Learn
  2. DSA
  3. Master level
All lessons in this course

Lesson 46 of 50 16 minMaster

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.

Searching for a word in a document, a gene in DNA, a code in a log file, a virus signature in a file: all are string matching. The text has length n, the pattern length m. The obvious method tries every alignment and compares letter by letter. Usually it fails fast, but on unlucky inputs (text AAAA…A, pattern AAAB) every alignment almost matches, and the cost is O(n·m).

BruteForce.cs
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;}
Output
0, 9, 12

Step through the three algorithms on the same input and compare the “letters compared” counter. Try your own text, such as AAAAAAAAAAAAAAAB with pattern AAAB, to see brute force at its worst.

Interactive

Finding a pattern in text

AABAACAADAABAABA
AABA
0123456789101112131415
Letters compared 0
Brute force: line the pattern up at every position and compare left to right.

KMP: never look back

When brute force hits a mismatch after matching j letters, it slides by one and re-reads letters it has already seen. But it already knows those j letters: they are the first j letters of the pattern. The Knuth-Morris-Pratt algorithm precomputes, for every prefix of the pattern, the length of its longest proper prefix that is also a suffix (the failure or LPS table). After a mismatch, the pattern slides so that this prefix lines up with the text, and comparison continues at the same text position.

PatternABABCABAB
LPS001201234

Read the table like this: after matching 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.

Kmp.cs
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;}
Output
0 0 1 2 0 1 2 3 4
10
0, 9, 12

The text index 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

Rabin-Karp turns strings into numbers with a hash: treat the letters as digits in base B and take the result modulo a large prime M. Comparing two numbers is O(1). The trick is the rolling hash: when the window slides one place right, the new hash is computed from the old one in O(1), by removing the left letter’s contribution and adding the new right letter.

RabinKarp.cs
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;}
Output
0, 9, 12

Rabin-Karp shines when searching for many patterns of the same length at once (put all pattern hashes in a hash set) and for finding repeated substrings, as plagiarism checkers and file-deduplication tools do.

In real .NET code

You rarely write these yourself. 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.

Ordinal.cs
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));
Output
4
True
AlgorithmTimeBest for
Brute forceO(n·m) worst, fast in practiceShort patterns, everyday text
KMPO(n + m) guaranteedStreams (never re-reads), worst-case guarantees
Rabin-KarpO(n + m) expectedMany patterns at once, repeated substrings
Boyer-MooreOften less than n comparisonsLong patterns over large alphabets; skips ahead
Aho-CorasickO(n + total pattern length + matches)Thousands of patterns: virus scanners, word filters
Quick check

Why is KMP O(n + m) even in the worst case?

Quick check

Two windows have the same Rabin-Karp hash as the pattern. What must the algorithm do?

Practice

Use the LPS table to find the shortest repeating unit of a string: ABCABCABC → ABC, ABAB → AB, ABCAB → ABCAB (no repetition), AAAA → A.

Show a hint

Let p = n − lps[n − 1]. If n is divisible by p, the first p letters repeat to form the whole string; otherwise the string is its own unit.

Show the solution
Solution.cs
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;}
Output
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.
0 of 50 lessons completed