Wiki
Advanced14 min read

String matching: naive, KMP and Rabin-Karp

Finding a pattern in a text: slide and compare naively, or reuse what was matched with KMP, or hash the windows with Rabin-Karp.

Searching for a word in a document is pattern matching: you have a text of length nn and a pattern of length mm, and you want every position where the pattern occurs. It is a problem you can state in one sentence and optimise for decades, and the optimisations generalise into the tools behind grep, DNA alignment and plagiarism detection.

Do not throw away what you just learned

The naive method compares, fails, and starts over one character later — even though it already matched several characters. KMP records how much of the pattern matched and jumps to a position that reuses that knowledge. The prefix function is a map of "if I failed here, where could I possibly be?".

Type a text and pattern below, then step through the KMP scan and compare the work against the naive method.

Scan with KMP and watch the prefix function prevent backtracking

ababcabcabababd
01234567891011121314
window starts at 0ababd

prefix function (LPS)

0, 0, 1, 2, 0

a matches a

KMP comparisons

19

naive comparisons

27

KMP never moves the text pointer backward: on a mismatch it consults LPS to reuse the part of the pattern already matched. On repetitive text that turns quadratic scanning into linear, which is why the gap above widens as the strings grow. Rabin-Karp instead hashes each window and only verifies on a hash hit.

Naive matching

Line the pattern up at each position and compare characters until a mismatch, then shift by one:

def naive(text, pattern):
    for i in range(len(text) - len(pattern) + 1):
        for j in range(len(pattern)):
            if text[i + j] != pattern[j]:
                break
        else:
            yield i

The worst case is O(nm)O(nm) — for example, a text of all as and a pattern of as ending in b, where every alignment scans almost the whole pattern before failing.

KMP and the prefix function

KMP preprocesses the pattern into a longest prefix-suffix (LPS) array. Entry lps[i] is the length of the longest proper prefix of pattern[0..i] that is also a suffix of it. On a mismatch at pattern index j, instead of restarting at the next text character, KMP sets j = lps[j-1] and keeps the text pointer where it is.

Because the text pointer never moves backward, the scan is O(n)O(n) after an O(m)O(m) preprocessing step — O(n+m)O(n + m) overall. The LPS array is the same "reuse overlapping structure" idea that appears in data compression and string algorithms generally.

Rabin-Karp and rolling hashes

Rabin-Karp avoids character comparisons almost entirely. It hashes the pattern and hashes each text window of length mm, using a rolling hash so the next window's hash is computed in O(1)O(1) from the previous one by removing the leading character and adding the trailing one:

hi=(hi−1−ti−1 b m−1) b+ti+m−1(modm)h_i = \big(h_{i-1} - t_{i-1}\,b^{\,m-1}\big)\,b + t_{i+m-1} \pmod m

On a hash match it verifies the actual characters, because different windows can collide. Expected time is O(n+m)O(n + m); a pathological input can force many verifications, but randomised moduli make that negligible. Rabin-Karp shines when searching for many patterns at once — hash them all and check the text's window hashes against the set.

A hash match is not a match

Two different strings can share a hash. Rabin-Karp must verify character by character on every hash hit, or it will report false positives. This is why the algorithm is probabilistic in its speed but exact in its answers.

In Python, ordinary substring search (sub in text, str.find) is implemented in C with an efficient two-way algorithm, so you rarely write KMP by hand. It matters when you need all occurrences, approximate matching, or the many-pattern case.

Illustrative vs real

The demo uses a short text so each comparison is visible and reports exact counts for this input, not a worst-case proof. Real texts are gigabytes, alphabets may be bytes rather than characters, and production libraries use SIMD and Boyer-Moore-style skipping. The core trade — naive rescan versus reuse or hash — is exactly what the panel computes.

Check yourself

Eduspheria wiki · Programming & Data Structures, Algorithms

0 / 5 answered

  1. 1What is the worst-case complexity of naive substring search?
    Multiple choice
  2. 2KMP can move the text pointer backward when a mismatch occurs.
    True / false
  3. 3What array preprocessing does KMP build from the pattern?
    Short answer
  4. 4A rolling hash of window length m updates using how many character operations per shift in big-O terms (give the number)?
    operations
    Numeric answer
  5. 5Why must Rabin-Karp verify characters after a hash match?
    Multiple choice

From the exam paper

Modeled on NITJ AI-507, End-Sem December 2024

0 / 4 answered

  1. 1In the sentence 'Advanced Data Structure is an interesting subject', what is the 0-based starting index of the first occurrence of the pattern 'Data'?
    Numeric answer
  2. 2How many times does the pattern 'Data' occur in that sentence?
    times
    Numeric answer
  3. 3Why can KMP scan the text without ever moving the text pointer backwards?
    Multiple choice
  4. 4Rabin-Karp must compare actual characters after a hash match, because two different strings can share the same hash.
    True / false

Where next: these primitives compose — heaps power Dijkstra, hashing powers Rabin-Karp, and every one of them rests on the asymptotic vocabulary from the start of this chapter.