๐ŸŽ“ A1Learner
๐Ÿ”

DSA KMP Pattern Matching

The Knuth-Morris-Pratt algorithm finds every occurrence of a pattern in a text while the text pointer only moves forward. Its prefix function records how much of the pattern stays matched after each mismatch.

The prefix table

Each table entry looks at the pattern prefix ending at that index. The entry for ababaca grows while the ab pattern repeats, then drops at the letter c.

def prefix_function(pattern):
    table = [0] * len(pattern)
    length = 0
    for i in range(1, len(pattern)):
        while length > 0 and pattern[i] != pattern[length]:
            length = table[length - 1]
        if pattern[i] == pattern[length]:
            length += 1
        table[i] = length
    return table


print("Pattern: ababaca")
print("Prefix table:", prefix_function("ababaca"))
print("Pattern: aabaab")
print("Prefix table:", prefix_function("aabaab"))

A mismatch falls back

The search below meets a wrong letter after matching ab. The table drops the matched length to a shorter prefix, and the same text letter is tried again.

def prefix_function(pattern):
    table = [0] * len(pattern)
    length = 0
    for i in range(1, len(pattern)):
        while length > 0 and pattern[i] != pattern[length]:
            length = table[length - 1]
        if pattern[i] == pattern[length]:
            length += 1
        table[i] = length
    return table


def find_first(text, pattern):
    table = prefix_function(pattern)
    matched = 0
    comparisons = 0
    for i in range(len(text)):
        while True:
            comparisons += 1
            if text[i] == pattern[matched]:
                matched += 1
                break
            if matched == 0:
                break
            matched = table[matched - 1]
        if matched == len(pattern):
            return i - len(pattern) + 1, comparisons
    return -1, comparisons


start, comparisons = find_first("abxababc", "ababc")
print("First match starts at:", start)
print("Comparisons used:", comparisons)

Overlaps survive

A full match also falls back through the table. The tail of one match can serve as the start of the next.

def prefix_function(pattern):
    table = [0] * len(pattern)
    length = 0
    for i in range(1, len(pattern)):
        while length > 0 and pattern[i] != pattern[length]:
            length = table[length - 1]
        if pattern[i] == pattern[length]:
            length += 1
        table[i] = length
    return table


def find_all(text, pattern):
    table = prefix_function(pattern)
    matched = 0
    found = []
    for i in range(len(text)):
        while matched > 0 and text[i] != pattern[matched]:
            matched = table[matched - 1]
        if text[i] == pattern[matched]:
            matched += 1
        if matched == len(pattern):
            found.append(i - len(pattern) + 1)
            matched = table[matched - 1]
    return found


print("Matches of aaa in aaaaa:", find_all("aaaaa", "aaa"))

Example

def prefix_function(pattern):
    table = [0] * len(pattern)
    length = 0
    for i in range(1, len(pattern)):
        while length > 0 and pattern[i] != pattern[length]:
            length = table[length - 1]
        if pattern[i] == pattern[length]:
            length += 1
        table[i] = length
    return table


def find_all_counted(text, pattern):
    table = prefix_function(pattern)
    matched = 0
    comparisons = 0
    found = []
    for i in range(len(text)):
        while True:
            comparisons += 1
            if text[i] == pattern[matched]:
                matched += 1
                break
            if matched == 0:
                break
            matched = table[matched - 1]
        if matched == len(pattern):
            found.append(i - len(pattern) + 1)
            matched = table[matched - 1]
    return found, comparisons


def find_all_naive(text, pattern):
    comparisons = 0
    found = []
    for start in range(len(text) - len(pattern) + 1):
        matched = 0
        while matched < len(pattern):
            comparisons += 1
            if text[start + matched] != pattern[matched]:
                break
            matched += 1
        if matched == len(pattern):
            found.append(start)
    return found, comparisons


text = "ababcabcababc"
pattern = "ababc"

print("Prefix table:", prefix_function(pattern))
matches, smart = find_all_counted(text, pattern)
plain_matches, plain = find_all_naive(text, pattern)
print("KMP matches:", matches)
print("Naive matches:", plain_matches)
print("KMP comparisons:", smart)
print("Naive comparisons:", plain)

Key points

  • Prefix function: for each pattern position, the length of the longest proper prefix that is also a suffix ending there
  • A proper prefix is a prefix that is not the whole string
  • On a mismatch, the matched length falls back through the table instead of restarting at zero
  • The text pointer never retreats, so the search makes one forward pass over the text
  • After a full match, the same fallback lets overlapping matches be found

In this example, the prefix table for ababc is printed first. Both searches report the same two match positions in the text. The counted comparisons separate them, because the naive search restarts at every position. KMP instead falls back through the table. It never rechecks a text letter against a prefix already ruled out.

Note

Building the table costs one pass over the pattern before the search starts

Try it Yourself

Edit the code, then press Run.

Running is simulated ยท Python Playground

Exercise

During a KMP search, the current text letter mismatches after four pattern letters matched. What happens next?