Pattern Matching (KMP, Rabin-Karp)
Pattern matching is the problem of finding all occurrences of a short string (needle / pattern) inside a longer string (haystack / text).
- Text length: n
- Pattern length: m
The naive algorithm runs in O(n·m). KMP and Rabin-Karp both achieve O(n + m) — but through completely different ideas. Knowing when to reach for each one is what interviewers test.
Naive Brute Force — O(n·m)
Try every possible starting position in the text; at each position compare the pattern
character-by-character. Simple but slow — in the worst case (e.g. text = "aaaa…a",
pattern = "aaa…ab") every alignment fails on the last character after m comparisons.
function naiveSearch(text, pattern) {
const n = text.length, m = pattern.length;
const matches = [];
for (let i = 0; i <= n - m; i++) {
let j = 0;
while (j < m && text[i + j] === pattern[j]) j++;
if (j === m) matches.push(i); // full match at index i
}
return matches;
}
console.log(naiveSearch('abcabcabc', 'abc')); // [0, 3, 6]
console.log(naiveSearch('aaaa', 'aa')); // [0, 1, 2]KMP — Knuth-Morris-Pratt
KMP eliminates redundant comparisons by pre-computing a failure function (also called the partial-match table or LPS — Longest Proper Prefix that is also a Suffix).
When a mismatch occurs at position j in the pattern, instead of restarting from scratch,
we jump to lps[j-1]. This means we never move the text pointer backwards.
Step 1 — Build the LPS (Failure Function)
lps[i] = length of the longest proper prefix of pattern[0..i] that is also a suffix.
"Proper" means the prefix cannot be the entire string.
// pattern = "abacaba"
// lps = [0, 0, 1, 0, 1, 2, 3]
//
// lps[6] = 3 because "aba" is both a prefix and suffix of "abacaba"
function buildLPS(pattern) {
const m = pattern.length;
const lps = new Array(m).fill(0);
let len = 0; // length of previous longest prefix-suffix
let i = 1;
while (i < m) {
if (pattern[i] === pattern[len]) {
lps[i] = ++len;
i++;
} else if (len !== 0) {
// Don't increment i — try a shorter prefix
len = lps[len - 1];
} else {
lps[i] = 0;
i++;
}
}
return lps;
}
console.log(buildLPS('abacaba')); // [0,0,1,0,1,2,3]
console.log(buildLPS('aabaabaaa')); // [0,1,0,1,2,3,4,5,2]Step 2 — KMP Search
function kmpSearch(text, pattern) {
const n = text.length, m = pattern.length;
if (m === 0) return [0];
const lps = buildLPS(pattern);
const matches = [];
let i = 0; // text index
let j = 0; // pattern index
while (i < n) {
if (text[i] === pattern[j]) {
i++;
j++;
}
if (j === m) {
matches.push(i - j); // match found
j = lps[j - 1]; // look for next match using failure function
} else if (i < n && text[i] !== pattern[j]) {
if (j !== 0) {
j = lps[j - 1]; // shift pattern, don't move i
} else {
i++;
}
}
}
return matches;
}
console.log(kmpSearch('abcabcabc', 'abc')); // [0, 3, 6]
console.log(kmpSearch('aabxaabyaab', 'aab')); // [0, 4, 8]Rabin-Karp — Rolling Hash
Rabin-Karp avoids character-by-character comparison by turning each window of length m into a hash value and comparing hashes instead.
The trick is a rolling hash: when the window slides one step right, you can compute the new hash in O(1) by:
- Subtracting the contribution of the leftmost character,
- Multiplying by the base,
- Adding the new rightmost character.
A hash match is followed by a full character comparison to rule out hash collisions.
function rabinKarp(text, pattern) {
const n = text.length, m = pattern.length;
if (m > n) return [];
const BASE = 31;
const MOD = 1_000_000_007n; // use BigInt to avoid JS float overflow
const base = BigInt(BASE);
const mod = MOD;
const a = BigInt('a'.charCodeAt(0) - 1);
// Precompute base^(m-1) mod MOD
let power = 1n;
for (let i = 0; i < m - 1; i++) power = (power * base) % mod;
// Hash of a single string window
function hash(s, start, len) {
let h = 0n;
for (let i = 0; i < len; i++) {
h = (h * base + BigInt(s.charCodeAt(start + i)) - a + 1n) % mod;
}
return h;
}
let patHash = hash(pattern, 0, m);
let winHash = hash(text, 0, m);
const matches = [];
for (let i = 0; i <= n - m; i++) {
if (winHash === patHash) {
// Hash match — verify with full comparison to handle collisions
if (text.slice(i, i + m) === pattern) matches.push(i);
}
if (i < n - m) {
// Roll the hash one step right
winHash = (
(winHash - (BigInt(text.charCodeAt(i)) - a + 1n) * power % mod + mod)
* base
+ BigInt(text.charCodeAt(i + m)) - a + 1n
) % mod;
}
}
return matches;
}
console.log(rabinKarp('abcabcabc', 'abc')); // [0, 3, 6]Z-Algorithm
The Z-algorithm computes a Z-array where Z[i] is the length of the longest
substring starting at i that is also a prefix of the string.
To find pattern P in text T, concatenate P + '$' + T (the $ acts as a sentinel
that cannot appear in either string), compute the Z-array, and every position where
Z[i] === m is a match.
function zArray(s) {
const n = s.length;
const Z = new Array(n).fill(0);
Z[0] = n;
let l = 0, r = 0;
for (let i = 1; i < n; i++) {
if (i < r) Z[i] = Math.min(r - i, Z[i - l]);
while (i + Z[i] < n && s[Z[i]] === s[i + Z[i]]) Z[i]++;
if (i + Z[i] > r) { l = i; r = i + Z[i]; }
}
return Z;
}
function zSearch(text, pattern) {
const combined = pattern + '$' + text;
const Z = zArray(combined);
const m = pattern.length;
const matches = [];
for (let i = m + 1; i < combined.length; i++) {
if (Z[i] === m) matches.push(i - m - 1); // offset back into text
}
return matches;
}
console.log(zSearch('aabxaab', 'aab')); // [0, 4]When to Use Each Algorithm
Algorithm | Best for | Avg Time | Worst Time | Space |
|---|---|---|---|---|
Naive | Short strings / interview warm-up | O(n+m) | O(n·m) | O(1) |
KMP | Single pattern search, repeated queries on same pattern | O(n+m) | O(n+m) | O(m) |
Rabin-Karp | Multi-pattern search (hash many patterns at once) | O(n+m) | O(n·m) | O(m) |
Z-Algorithm | Pattern in string, prefix-suffix queries | O(n+m) | O(n+m) | O(n+m) |
Practice Problems
LeetCode 28 — Find the Index of the First Occurrence in a String (KMP)
LeetCode 459 — Repeated Substring Pattern (KMP / Z-algorithm)
LeetCode 686 — Repeated String Match (rotation + KMP)
LeetCode 1392 — Longest Happy Prefix (build LPS array directly)
LeetCode 214 — Shortest Palindrome (KMP on reversed string)