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.

22 minLesson 1 of 3
On this page
  1. The naive way: try every start
  2. KMP: never read a character twice
  3. Rabin-Karp: compare fingerprints
  4. Common mistakes
  5. Check yourself
  6. What to practise next

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.

Naive search for HHTHHHUse the arrows or ← → keys
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
  1. text
    0H
    1H
    2T
    3H
    4H
    5T
    6H
    7H
    8H
    pattern
    0Hstart
    1H
    2T
    3H
    4H
    5H
    6
    7
    8
    comparisons
    6

    1Start at 0. The first five characters match, then text T meets pattern H. 6 comparisons so far.

  2. text
    0H
    1H
    2T
    3H
    4H
    5T
    6H
    7H
    8H
    pattern
    0
    1Hstart
    2H
    3T
    4H
    5H
    6H
    7
    8
    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 T against H fails.

  3. text
    0H
    1H
    2T
    3H
    4H
    5T
    6H
    7H
    8H
    pattern
    0
    1
    2Hstart
    3H
    4T
    5H
    6H
    7H
    8
    comparisons
    9

    3Start 2: T against H fails straight away.

  4. text
    0H
    1H
    2T
    3H
    4H
    5T
    6H
    7H
    8H
    pattern
    0
    1
    2
    3Hstart
    4H
    5T
    6H
    7H
    8H
    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.

1 / 4

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.

Naive search
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.

text H H T H H T H H H before H H T H H H after H H T H H H compare here next
After matching HHTHH, its longest border is HH. Slide the pattern so its prefix HH sits under the suffix HH and continue from there.

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.

Failure table for HHTHHHUse the arrows or ← → keys
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
  1. pattern
    0Hk
    1H
    2T
    3H
    4H
    5H
    table
    00
    1
    2
    3
    4
    5
    k
    0

    1A single character has no border: table[0] = 0, k = 0.

  2. pattern
    0Hk
    1Hi
    2T
    3H
    4H
    5H
    table
    00
    11
    2
    3
    4
    5
    k
    1

    2p[1] is H and p[0] is H. They match, so the border grows to 1: in HH, the first H is also the last.

  3. pattern
    0H
    1Hk
    2Ti
    3H
    4H
    5H
    table
    00
    11
    2
    3
    4
    5
    k
    0

    3p[2] is T but p[1] is H. The border cannot grow, so fall back: k = table[0] = 0.

  4. pattern
    0Hk
    1H
    2Ti
    3H
    4H
    5H
    table
    00
    11
    20
    3
    4
    5
    k
    0

    4Compare T with p[0], an H. No match, so table[2] = 0.

  5. pattern
    0Hk
    1H
    2T
    3Hi
    4H
    5H
    table
    00
    11
    20
    31
    4
    5
    k
    1

    5p[3] is H, matching p[0]. Border length 1.

  6. pattern
    0H
    1Hk
    2T
    3H
    4Hi
    5H
    table
    00
    11
    20
    31
    42
    5
    k
    2

    6p[4] is H, matching p[1]. Border length 2: HHTHH starts and ends with HH.

  7. pattern
    0H
    1H
    2Tk
    3H
    4H
    5Hi
    table
    00
    11
    20
    31
    42
    5
    k
    1

    7p[5] is H but p[2] is T, so HHT is not a border. Fall back to the border of HH: k = table[1] = 1.

  8. pattern
    0H
    1Hk
    2T
    3H
    4H
    5Hi
    table
    00
    11
    20
    31
    42
    52
    k
    2

    8Now compare H with p[1], an H. Match, so k = 2 and table[5] = 2. The table is done: [0, 1, 0, 1, 2, 2].

1 / 8

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.

KMP search for HHTHHHUse the arrows or ← → keys
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
  1. text
    0Hi
    1H
    2T
    3H
    4H
    5T
    6H
    7H
    8H
    pattern
    0H
    1H
    2T
    3H
    4H
    5H
    6
    7
    8
    j
    0
    comparisons
    0

    1Pattern under text position 0, nothing matched yet.

  2. text
    0H
    1H
    2T
    3H
    4Hi
    5T
    6H
    7H
    8H
    pattern
    0H
    1H
    2T
    3H
    4H
    5H
    6
    7
    8
    j
    5
    comparisons
    5

    2Text positions 0 to 4 match the first five pattern characters, one comparison each. j = 5.

  3. text
    0H
    1H
    2T
    3H
    4H
    5Ti
    6H
    7H
    8H
    pattern
    0H
    1H
    2T
    3H
    4H
    5H
    6
    7
    8
    j
    5
    comparisons
    6

    3At i = 5, text T meets pattern H. Mismatch.

  4. text
    0H
    1H
    2T
    3H
    4H
    5Ti
    6H
    7H
    8H
    pattern
    0
    1
    2
    3H
    4H
    5T
    6H
    7H
    8H
    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, so i stays at 5.

  5. text
    0H
    1H
    2T
    3H
    4H
    5Ti
    6H
    7H
    8H
    pattern
    0
    1
    2
    3H
    4H
    5T
    6H
    7H
    8H
    j
    3
    comparisons
    7

    5Text T against pattern T: match. j = 3.

  6. text
    0H
    1H
    2T
    3H
    4H
    5T
    6H
    7H
    8Hi
    pattern
    0
    1
    2
    3H
    4H
    5T
    6H
    7H
    8H
    j
    6
    comparisons
    10

    6Positions 6, 7 and 8 match H, H, H. j reaches 6, the full pattern.

  7. text
    0H
    1H
    2T
    3H
    4H
    5T
    6H
    7H
    8H
    pattern
    0
    1
    2
    3H
    4H
    5T
    6H
    7H
    8H
    found at
    3
    comparisons
    10
    naive
    15

    7Found at 8 − 6 + 1 = 3. That took 10 comparisons instead of the naive 15, and i never moved backwards.

1 / 7
KMP
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 found

The 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.

Rolling the window to find 265Use the arrows or ← → keys
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]
  1. text
    03
    11
    24
    31
    45
    59
    62
    76
    85
    93
    105
    target
    265
    h
    314

    1Compute the pattern's number once: 265. The first window is 314. Not equal.

  2. text
    03
    11
    24
    31
    45
    59
    62
    76
    85
    93
    105
    target
    265
    h
    141

    2Roll: (314 − 300) × 10 + 1 = 141. Three operations, whatever the window length.

  3. text
    03
    11
    24
    31
    45
    59
    62
    76
    85
    93
    105
    target
    265
    h
    415

    3(141 − 100) × 10 + 5 = 415. Not equal.

  4. text
    03
    11
    24
    31
    45
    59
    62
    76
    85
    93
    105
    target
    265
    h
    159

    4(415 − 400) × 10 + 9 = 159.

  5. text
    03
    11
    24
    31
    45
    59
    62
    76
    85
    93
    105
    target
    265
    h
    592

    5(159 − 100) × 10 + 2 = 592.

  6. text
    03
    11
    24
    31
    45
    59
    62
    76
    85
    93
    105
    target
    265
    h
    926

    6(592 − 500) × 10 + 6 = 926.

  7. text
    03
    11
    24
    31
    45
    59
    62
    76
    85
    93
    105
    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.

1 / 7

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.

Rabin-Karp
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
ApproachTimeSpaceWhy
NaiveO(n·m)O(1)Up to m comparisons at each of about n starts.
KMPO(n + m)O(m)Table in O(m); the text pointer never moves back.
Rabin-KarpO(n + m) expectedO(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

Quick check

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.