📒 problems 📖 glossary
Mock interview
46:30
ready
of 46:30
Lesson 30 · Strings & math

String matching beyond brute force

📖 Walk me through it — plain English

The question is simple: does a small string (the pattern, call it P) appear somewhere inside a bigger string (the text, call it T)? The obvious way is to line P up at position 0 of T, compare character by character, and if anything mismatches, slide P over by one and start the comparison over from scratch. With T of length n and P of length m, that re-checking can cost up to O(n·m) — for every one of the n starting spots you might re-scan all m characters. The two methods here, KMP and Rabin-Karp, both get you to O(n+m): they avoid redoing work.

Analogy for KMP: imagine you're looking for the word "ababa" in a long sentence, reading left to right, and you've already matched "abab" before hitting a wrong letter. The naive reader throws all that progress away and restarts one letter later. But you're smarter — you notice the "ab" at the end of what you matched is the same as the "ab" at the start of the pattern, so you don't need to re-read those; you can keep that partial credit and continue. KMP precomputes exactly how much partial credit you keep at each point. That precomputed table is the failure function (the array fail in the code).

Definition, slowly: fail[i] is the length of the longest proper prefix of P[0..i] that is also a suffix of it. "Prefix" = a chunk starting at the front. "Suffix" = a chunk ending at the back. "Proper" = not the whole string itself. So it answers: "of the first i+1 characters I've matched, how many characters at the front also appear, identically, at the back?" That overlap is what lets us slide the pattern forward without rescanning. Now let's trace kmp_failure on the tiny pattern P = "ababa". The variable k tracks the current overlap length. We start fail[0] = 0 (a single character has no proper prefix), then walk i from 1 to the end. The highlighted cell is the index i we're filling right now.

Step 0 · The pattern. Below each letter we'll fill in fail[i]. fail[0] = 0 by definition.
a
b
a
b
a
0
?
?
?
?
Step 1 · i=1 (letter 'b'), k=0. We compare p[0]='a' with p[1]='b'. They differ, so the overlap stays 0. fail[1] = 0.
a
b
a
b
a
0
0
?
?
?
Step 2 · i=2 (letter 'a'), k=0. Compare p[0]='a' with p[2]='a' — match! Bump k to 1. fail[2] = 1. Meaning: "a" at the front also sits at the back.
a
b
a
b
a
0
0
1
?
?
Step 3 · i=3 (letter 'b'), k=1. Compare p[1]='b' with p[3]='b' — match! Bump k to 2. fail[3] = 2. The front "ab" matches the back "ab".
a
b
a
b
a
0
0
1
2
?
Step 4 · i=4 (letter 'a'), k=2. Compare p[2]='a' with p[4]='a' — match! Bump k to 3. fail[4] = 3. The front "aba" matches the back "aba".
a
b
a
b
a
0
0
1
2
3
Done · fail = [0, 0, 1, 2, 3]. Now in kmp_search, if matching "ababa" against the text breaks after 'ababa…' partway, we consult fail and resume from the saved overlap (k = fail[k-1]) instead of restarting at 0.
0
0
1
2
3

Why this is fast: in the search loop, k only goes up by 1 per character read, and each time we mismatch we shrink k via the failure table — never re-reading text characters. Total work is proportional to the length of the text plus the length of the pattern, i.e. O(n+m). The while k > 0 loop looks like it could be slow, but across the whole run it can only undo increments that already happened, so it's bounded overall.

The second method, Rabin-Karp, takes a totally different angle: turn each window of m characters into a single number (a hash), like reading the characters as digits of a base-257 number taken modulo a big prime M. Comparing two numbers is one O(1) step. As the window slides right one spot, you subtract the leaving character's contribution, multiply by the base, and add the new character — also O(1) — so you don't recompute the whole window. Because two different strings can occasionally land on the same number (a collision), whenever the hashes match you do one real character-by-character comparison (text[i:i+m] == p) to confirm it's a true match and not a coincidence. With a good prime, collisions are rare, so this averages O(n+m) too.

"Does pattern P appear in text T?" Naive is O(n·m). KMP and rolling hash are both O(n+m).

KMP — failure function

Precompute fail[i] = length of the longest proper prefix of P[0..i] that is also a suffix. When the pattern mismatches at position j in T, slide P by j - fail[j-1] instead of restarting.

P: ababcabab fail: 001201234 at index 7 (a), the longest prefix==suffix is "abab" → length 4
def kmp_failure(p):
    fail = [0] * len(p)
    k = 0
    for i in range(1, len(p)):
        while k > 0 and p[k] != p[i]:
            k = fail[k - 1]
        if p[k] == p[i]: k += 1
        fail[i] = k
    return fail

def kmp_search(text, p):
    fail = kmp_failure(p)
    k = 0
    for i, ch in enumerate(text):
        while k > 0 and p[k] != ch:
            k = fail[k - 1]
        if p[k] == ch: k += 1
        if k == len(p): return i - k + 1   # match start
    return -1
Rolling hash — Rabin-Karp

Treat the substring as a base-B number mod M. As the window slides, drop the leading character and append the next in O(1). Hash collisions need a real comparison to confirm.

def rabin_karp(text, p, B=257, M=10**9+7):
    n, m = len(text), len(p)
    if m > n: return -1
    hp = h = 0
    high = pow(B, m - 1, M)
    for i in range(m):
        hp = (hp * B + ord(p[i])) % M
        h  = (h  * B + ord(text[i])) % M
    for i in range(n - m + 1):
        if h == hp and text[i:i+m] == p:   # collision check
            return i
        if i + m < n:
            h = ((h - ord(text[i]) * high) * B + ord(text[i+m])) % M
    return -1

Find the first occurrence of a needle in a haystack:

→ Going deeper: Hashing strings is O(n); prefix queries want a trie. See Tries (prefix trees).