String Matching
Find a pattern inside a long text: the naive scan, KMP with its failure table traced step by step, and Rabin-Karp's rolling hash, in JS and Python.
On this page
You flip a coin all afternoon and write down every result: HHTHHTHHH. A friend bets that the run HHTHHH will show up somewhere. Did it, and where?
That is string matching: find every place a short pattern of length m appears in a long text of length n. Your editor's Find box does it, and so does a genome search. Those texts get huge, so the method matters.
The naive way: try every start
Line the pattern up under the text at position 0 and compare character by character. On the first mismatch, slide the pattern one step right and start again from its first character.
for start from 0 to n - m: k = 0 while k < m and text[start + k] == p[k]: k = k + 1 if k == m: record start- text0H1H2T3H4H5T6H7H8Hpattern0Hstart1H2T3H4H5H678
- comparisons
- 6
1Start at 0. The first five characters match, then text
Tmeets patternH. 6 comparisons so far. - text0H1H2T3H4H5T6H7H8Hpattern01Hstart2H3T4H5H6H78
- comparisons
- 8
2Slide to start 1 and begin again from the pattern's first character. Text positions 1 and 2 are read a second time, and
TagainstHfails. - text0H1H2T3H4H5T6H7H8Hpattern012Hstart3H4T5H6H7H8
- comparisons
- 9
3Start 2:
TagainstHfails straight away. - text0H1H2T3H4H5T6H7H8Hpattern0123Hstart4H5T6H7H8H
- comparisons
- 15
- found at
- 3
4Start 3: all six characters match. The pattern is at position 3, after 15 comparisons on a text of only 9 characters.
Each of about n starts can cost up to m comparisons: O(n·m). Search a million Hs for HHHHHHHHHT and every start matches nine characters before failing.
function naiveSearch(text, pattern) {
if (pattern.length === 0) return [];
const found = [];
for (let start = 0; start + pattern.length <= text.length; start++) {
let k = 0;
while (k < pattern.length && text[start + k] === pattern[k]) k++;
if (k === pattern.length) found.push(start);
}
return found;
}
naiveSearch("HHTHHTHHH", "HHTHHH"); // [3]def naive_search(text, pattern):
if not pattern:
return []
found = []
for start in range(len(text) - len(pattern) + 1):
k = 0
while k < len(pattern) and text[start + k] == pattern[k]:
k += 1
if k == len(pattern):
found.append(start)
return found
naive_search("HHTHHTHHH", "HHTHHH") # [3]KMP: never read a character twice
Look at the first step again. When the mismatch happened, we had just matched HHTHH. We know exactly what those five text characters are, because they equal the start of the pattern. Throwing that knowledge away and re-reading them is the waste.
The Knuth-Morris-Pratt algorithm (KMP) keeps it. The matched part HHTHH ends with HH, and the pattern also starts with HH. So slide the pattern until its first HH sits under those last two text characters. They already match, so carry on comparing from pattern position 2. The text pointer never moves backwards.
A piece that is both a prefix and a suffix of a string (but not the whole string) is a border. HHTHH has borders H and HH. Use the longest border: it gives the smallest slide, so no real match is skipped.
The borders depend only on the pattern, never on the text. So KMP works them out once, before searching, and stores them in the failure table: table[i] is the length of the longest border of pattern[0..i].
Building the failure table
k is the length of the border found so far. At each new character, try to extend that border by one. If it does not fit, fall back to the border of the border, table[k − 1], and try again.
table[0] = 0, k = 0for i from 1 to m - 1: while k > 0 and p[i] != p[k]: k = table[k - 1] if p[i] == p[k]: k = k + 1 table[i] = k- pattern0Hk1H2T3H4H5Htable0012345
- k
- 0
1A single character has no border:
table[0] = 0,k = 0. - pattern0Hk1Hi2T3H4H5Htable00112345
- k
- 1
2
p[1]is H andp[0]is H. They match, so the border grows to 1: inHH, the first H is also the last. - pattern0H1Hk2Ti3H4H5Htable00112345
- k
- 0
3
p[2]is T butp[1]is H. The border cannot grow, so fall back:k = table[0] = 0. - pattern0Hk1H2Ti3H4H5Htable001120345
- k
- 0
4Compare T with
p[0], an H. No match, sotable[2] = 0. - pattern0Hk1H2T3Hi4H5Htable0011203145
- k
- 1
5
p[3]is H, matchingp[0]. Border length 1. - pattern0H1Hk2T3H4Hi5Htable00112031425
- k
- 2
6
p[4]is H, matchingp[1]. Border length 2:HHTHHstarts and ends withHH. - pattern0H1H2Tk3H4H5Hitable00112031425
- k
- 1
7
p[5]is H butp[2]is T, soHHTis not a border. Fall back to the border ofHH:k = table[1] = 1. - pattern0H1Hk2T3H4H5Hitable001120314252
- k
- 2
8Now compare H with
p[1], an H. Match, sok = 2andtable[5] = 2. The table is done:[0, 1, 0, 1, 2, 2].
Searching with the table
The search loop has the same shape. j counts how many pattern characters currently match. On a mismatch, j drops to table[j − 1] while i, the text position, stays put.
j = 0for i from 0 to n - 1: while j > 0 and text[i] != p[j]: j = table[j - 1] if text[i] == p[j]: j = j + 1 if j == m: record i - m + 1- text0Hi1H2T3H4H5T6H7H8Hpattern0H1H2T3H4H5H678
- j
- 0
- comparisons
- 0
1Pattern under text position 0, nothing matched yet.
- text0H1H2T3H4Hi5T6H7H8Hpattern0H1H2T3H4H5H678
- j
- 5
- comparisons
- 5
2Text positions 0 to 4 match the first five pattern characters, one comparison each.
j = 5. - text0H1H2T3H4H5Ti6H7H8Hpattern0H1H2T3H4H5H678
- j
- 5
- comparisons
- 6
3At
i = 5, text T meets pattern H. Mismatch. - text0H1H2T3H4H5Ti6H7H8Hpattern0123H4H5T6H7H8H
- j
- 2
- comparisons
- 6
4Fall back:
j = table[4] = 2. The pattern slides to start 3. Text positions 3 and 4 are known to be HH, soistays at 5. - text0H1H2T3H4H5Ti6H7H8Hpattern0123H4H5T6H7H8H
- j
- 3
- comparisons
- 7
5Text T against pattern T: match.
j = 3. - text0H1H2T3H4H5T6H7H8Hipattern0123H4H5T6H7H8H
- j
- 6
- comparisons
- 10
6Positions 6, 7 and 8 match H, H, H.
jreaches 6, the full pattern. - text0H1H2T3H4H5T6H7H8Hpattern0123H4H5T6H7H8H
- found at
- 3
- comparisons
- 10
- naive
- 15
7Found at 8 − 6 + 1 = 3. That took 10 comparisons instead of the naive 15, and
inever moved backwards.
function buildTable(pattern) {
const table = new Array(pattern.length).fill(0);
let k = 0; // length of the current border
for (let i = 1; i < pattern.length; i++) {
while (k > 0 && pattern[i] !== pattern[k]) k = table[k - 1];
if (pattern[i] === pattern[k]) k++;
table[i] = k;
}
return table;
}
function kmpSearch(text, pattern) {
if (pattern.length === 0) return [];
const table = buildTable(pattern);
const found = [];
let j = 0; // pattern characters matched so far
for (let i = 0; i < text.length; i++) {
while (j > 0 && text[i] !== pattern[j]) j = table[j - 1];
if (text[i] === pattern[j]) j++;
if (j === pattern.length) {
found.push(i - j + 1);
j = table[j - 1]; // keep going: matches can overlap
}
}
return found;
}def build_table(pattern):
table = [0] * len(pattern)
k = 0 # length of the current border
for i in range(1, len(pattern)):
while k > 0 and pattern[i] != pattern[k]:
k = table[k - 1]
if pattern[i] == pattern[k]:
k += 1
table[i] = k
return table
def kmp_search(text, pattern):
if not pattern:
return []
table = build_table(pattern)
found = []
j = 0 # pattern characters matched so far
for i, ch in enumerate(text):
while j > 0 and ch != pattern[j]:
j = table[j - 1]
if ch == pattern[j]:
j += 1
if j == len(pattern):
found.append(i - j + 1)
j = table[j - 1] # keep going: matches can overlap
return foundThe inner while looks like it could make this O(n·m). It cannot. j rises by at most 1 per text character, and every fallback lowers it. It cannot fall more often than it rose, so the whole search is at most 2n steps. The table build works the same way. It is the "count total moves" argument from two pointers.
Rabin-Karp: compare fingerprints
A completely different idea: turn each window of m characters into a number, a hash, and compare numbers instead of strings. Two numbers are compared in one step. The trick is computing each window's number in O(1) from the previous one.
Try it with digits: search for 265 in 31415926535. The first window reads 314. To slide one step, drop the leading 3, shift left, and bring in the next digit: (314 − 3 × 100) × 10 + 1 = 141. That is a rolling hash.
target = value(p), h = value(text[0..m-1])for start from 0 to n - m: if h == target and window == p: record start h = (h - text[start] * 100) * 10 + text[start + m]- text03112431455962768593105
- target
- 265
- h
- 314
1Compute the pattern's number once: 265. The first window is 314. Not equal.
- text03112431455962768593105
- target
- 265
- h
- 141
2Roll: (314 − 300) × 10 + 1 = 141. Three operations, whatever the window length.
- text03112431455962768593105
- target
- 265
- h
- 415
3(141 − 100) × 10 + 5 = 415. Not equal.
- text03112431455962768593105
- target
- 265
- h
- 159
4(415 − 400) × 10 + 9 = 159.
- text03112431455962768593105
- target
- 265
- h
- 592
5(159 − 100) × 10 + 2 = 592.
- text03112431455962768593105
- target
- 265
- h
- 926
6(592 − 500) × 10 + 6 = 926.
- text03112431455962768593105
- target
- 265
- h
- 265
- found at
- 6
7(926 − 900) × 10 + 5 = 265. The numbers match, so check the three characters to be sure. They match: found at position 6, after 7 windows.
Real text works the same way: treat each character code as a digit in base 256. Those numbers get huge, so keep every value modulo a large prime such as 1,000,000,007.
The modulo has a price: different windows can share a hash. With modulo 7, 314 and 265 both leave remainder 6. So a hash match only means "maybe". Always confirm by comparing the actual characters. When hashes differ, the strings are certainly different, and that is where the speed comes from.
function rabinKarp(text, pattern) {
const n = text.length, m = pattern.length;
const BASE = 256, MOD = 1_000_000_007;
if (m === 0 || m > n) return [];
let high = 1; // BASE^(m-1) % MOD: the weight of the leading character
for (let i = 0; i < m - 1; i++) high = (high * BASE) % MOD;
let target = 0, h = 0;
for (let i = 0; i < m; i++) {
target = (target * BASE + pattern.charCodeAt(i)) % MOD;
h = (h * BASE + text.charCodeAt(i)) % MOD;
}
const found = [];
for (let start = 0; start + m <= n; start++) {
if (h === target && text.slice(start, start + m) === pattern) found.push(start);
if (start + m < n) {
h = (h - (text.charCodeAt(start) * high) % MOD + MOD) % MOD; // drop the left char
h = (h * BASE + text.charCodeAt(start + m)) % MOD; // add the right char
}
}
return found;
}def rabin_karp(text, pattern):
n, m = len(text), len(pattern)
BASE, MOD = 256, 1_000_000_007
if m == 0 or m > n:
return []
high = pow(BASE, m - 1, MOD) # the weight of the leading character
target = h = 0
for i in range(m):
target = (target * BASE + ord(pattern[i])) % MOD
h = (h * BASE + ord(text[i])) % MOD
found = []
for start in range(n - m + 1):
if h == target and text[start:start + m] == pattern:
found.append(start)
if start + m < n:
h = (h - ord(text[start]) * high) % MOD # drop the left char
h = (h * BASE + ord(text[start + m])) % MOD # add the right char
return found| Approach | Time | Space | Why |
|---|---|---|---|
| Naive | O(n·m) | O(1) | Up to m comparisons at each of about n starts. |
| KMP | O(n + m) | O(m) | Table in O(m); the text pointer never moves back. |
| Rabin-Karp | O(n + m) expected | O(1) | O(1) per roll, plus m for each hash match. O(n·m) if most windows match or collide. |
For a single everyday search, use your language's built-in (indexOf, str.find). Write KMP when you need a guaranteed O(n + m), or the failure table itself. Reach for Rabin-Karp when you compare many windows at once, such as finding a repeated substring of length m: put every window's hash in a hash set.
Common mistakes
Check yourself
What is the failure table for the pattern ABAB?
KMP has a while loop inside a for loop over the text. Why is the search still O(n)?
In Rabin-Karp, a window's hash equals the pattern's hash. What do you know?
What to practise next
Build the failure table for ABCABD by hand, then check it with the code. Then move on to Segment Trees and Fenwick Trees, which answer range questions on data that keeps changing.