Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save AdityaSoni19031997/0916ddf3d0918d4980eac13d3411fbfa to your computer and use it in GitHub Desktop.
Save AdityaSoni19031997/0916ddf3d0918d4980eac13d3411fbfa to your computer and use it in GitHub Desktop.
Python - Knuth-Morris-Pratt (KMP) string-matching algorithm
# File: KnuthMorrisPratt.py
# Author: Keith Schwarz ([email protected])
#
# An implementation of the Knuth-Morris-Pratt (KMP) string-matching algorithm.
# This algorithm takes as input a pattern string P and target string T, then
# finds the first occurrence of the string T in the pattern P, doing so in time
# O(|P| + |T|). The logic behind the algorithm is not particularly complex,
# though getting it to run in linear time requires a few non-obvious tricks.
#
# To motivate KMP, consider the naive algorithm for trying to match a pattern
# string P against a target T. This would work by considering all possible
# start positions for the pattern P in the target T, then checking whether a
# match exists at each of those positions. For example, to match the string
# ABC against the target string ABABABACCABC, we'd get
#
# ABABABACCABC
# ABX (first two characters match, last does not)
# X (first character doesn't match)
# ABX (first two characters match, last does not)
# X (first character doesn't match)
# ABX (first two characters match, last does not)
# X (first character doesn't match)
# AX (first character matches, second doesn't)
# X (first character doesn't match)
# X (first character doesn't match)
# ABC (match found)
#
# This algorithm runs in O(mn) in the worst case, where m = |T| and n = |P|,
# because it has to do O(n) work to check whether the string matches O(m) times
# for each spot in the string.
#
# However, a lot of this is wasted work. For example, in the above example,
# consider what happens when we know that the string ABC does not match the
# first part of the string, ABA. At this point, it would be silly to actually
# try to match the string at the string starting with the B, since there's no
# possible way that the string could match there. Instead, it would make more
# sense to instead start over and try matching ABC at the next A. In fact,
# more generally, if we can use the information we have about what characters
# we already matched to determine where we should try to resume the search in
# the string, we can avoid revisiting characters multiple times when there's no
# hope that they could ever match.
#
# The idea we'll use is to look for "borders" of a string, which are substrings
# that are both a prefix and suffix of the string. For example, the string
# "aabcaa" has "aa" as a border, while the string "abc" just has the empty
# string as a border. Borders are useful in KMP because they encode
# information about where we might need to pick up the search when a particular
# match attempt fails. For example, suppose that we want to match ABABC
# against the string ABABABC. If we start off by trying to match the string,
# we'll find that they overlap like this:
#
# ABABABC
# ABABx
#
# That is, the first four characters match, but the fifth does not. At this
# point, rather than naively restarting the search at the second character (B),
# or even restarting it at the third position (A), we can instead note that we
# can treat the last two characters of the string we matched (AB) as the first
# two characters of the pattern string ABABC if we just treated it instead as
# though we had
#
# ABABABC
# ABABx
# ABABC
#
# If we can somehow remember the fact that we already matched the AB at the
# start of this string, we could just confirm that the three characters after
# it are ABC and be done. There's no need to confirm that the characters at
# the front match.
#
# In order to make this possible, we'll construct a special data structure
# called the "fail table." This table stores, for each possible prefix of the
# string to match, the length of the longest border of that prefix. That way,
# when we find a mismatch, we know where the next possible start location could
# be found. In particular, once we have a mismatch, if there's any border of
# the prefix of the pattern that we matched so far, then we can treat the end
# of that matching prefix as the start of a prefix of the word that occurs
# later in the target.
#
# The basic idea behind KMP is, given this table, to execute the following:
#
# - Guess that the string starts at the beginning of the target.
# - Match as much of the string as possible.
# - If the whole string matched, we're done.
# - Otherwise, a mismatch was found. Look up the largest border of the
# string that was matched so far in the failure table. Suppose it has
# length k.
# - Update our guess of the start position to be where that border occurs
# in the portion matched so far, then repeat this process.
#
# Notice that once we've matched a character against part of the pattern (or
# found that it can't possibly match), we never visit that character again.
# This is responsible for the fast runtime of the algorithm (though I'll give a
# more formal description later on).
# Function: failTable(pattern)
# Usage: failTable("This is a string!")
# -----------------------------------------------------------------------------
# Given a string, constructs the KMP failure table for that string. The values
# in the table are defined as
#
# table[i] = |LongestProperBoundary(pattern[0:i)])|
#
# Where the longest proper boundary of a string is the longest proper substring
# of that string that is both a prefix and a suffix. For example, given the
# string "abcabc," the longest proper boundary is abc. Similarly, given the
# string "apple," the longest proper boundary is the empty string.
#
# As a sample output of this function, given the string "ababcac", the table
# would be
#
# a b a b c a c
# * 0 0 1 2 0 1 0
#
# This means, for example, that the longest proper boundary of the prefix "aba"
# has length 1, while the longest proper boundary of the string as a whole is
# the empty string. Notice that the first entry is *, which we have chosen
# because there is no mathematically well-defined proper substring of the empty
# string. We can put anything we want there, and we'll go with None.
#
# To compute the values of this table, we use a dynamic programming algorithm
# to compute a slightly stronger version of the function. We define the
# function "Extended Longest Proper Boundary" (xLPB) as follows:
#
# xLPB(string, n, char) = The longest proper boundary of string[0:n] + char
#
# The idea behind this function is that we want to be able to recycle the
# values of the longest proper boundary function for smaller prefixes of the
# string in order to compute the longest proper boundary for longer prefixes.
# To make this easier, the xLPB function allows us to talk about what would
# happen if we extended the longest proper boundary of some prefix of the
# string by a single character. Notice that for any nonzero n, we have that
#
# LongestProperBoundary(string[0:n]) = xLPB(string, n - 1, string[n])
#
# That is, we simply tear off the last character and use it as the final
# argument to xLPB. Given this xLPB function, we can compute its values
# recursively using the following logic. As a base case, xLPB(string, 0, char)
# is the longest proper boundary of string[0:0] + char = char. But this has
# only one proper boundary, the empty string, and so its value must be zero.
#
# Now suppose that for all n' < n we have the value of xLPB(string, n', char)
# for any character char. Suppose we want to go and compute
# xLPB(string, n, char). Let's think about what this would mean. Given that
# n is not zero, we can think of this problem as trying to find the longest
# proper boundary of this string:
#
# +------------+---+------------+------------+---+
# | LPB | ? | ... | LPB | c |
# +------------+---+------------+------------+---+
#
# ^ ^ ^
# +----------------------+-------------------+ |
# | |
# String of length n New character
#
# The idea is that we have the original string of length n, followed by our new
# character char (which we'll abbreviate c). In this diagram, I've marked the
# LPB of the string of length n. Notice that right after the LPB at the prefix
# of the string, we have some character whose value is unknown (since n != 0
# and the LPB can't be the whole string). If this value is equal to c, then
# the LPB of the whole string can be formed by simply extending the LPB of the
# first n characters. There can't be a longer proper boundary, since otherwise
# we could show that by taking that longer boundary and dropping off the
# character c, we'd end up with a longer proper boundary for the first n
# characters of the string, contradicting that we chose the longest proper
# boundary.
#
# By our above argument, remember that the length of the longest proper
# boundary of the first n characters of the string is given by
#
# xLPB(string, n - 1, string[n - 1])
#
# Thus we have the first part of our recurrence, which is defined as
#
# xLPB(string, n, char) =
# if n = 0, then 0.
# let k = xLPB(string, n - 1, string[n - 1])
# if string[k] == char, return k + 1
# else, ???
#
# Now, suppose that we find that the character after the LPB does not match.
# If this happens, we can then make the following observation. Below I've
# reprinted the above diagram:
#
# +------------+---+------------+------------+---+
# | LPB | ? | ... | LPB | c |
# +------------+---+------------+------------+---+
#
# ^ ^ ^
# +----------------------+-------------------+ |
# | |
# String of length n New character
#
# Notice that any LPB of this new string must be a prefix of the LPB of the
# first n characters and a suffix of the LPB followed by the character c.
# Since by definition the LPB of the first n characters must be a prefix of
# those n characters, we have the following elegant conclusion to our
# recurrence:
#
# xLPB(string, n, char) =
# if n = 0, then 0.
# let k = xLPB(string, n - 1, string[n - 1])
# if string[k] == char, return k + 1
# else, xLPB(string, k, char)
#
# The reason for this is that xLPB(string, k, char) asks for the longest
# proper boundary of the LPB of the string formed from the first n characters
# of the string followed by the character c, which is exactly what we described
# above.
#
# As written, filling in the table of LPB values would take O(n^2) time, where
# n is the length of the string. However, using dynamic programming and an
# amortized analysis, we can show that this function can be made to run in
# O(n) time. In particular, suppose that for all n' < n, we know the value of
# LPB(string[0:n]). Then in the above formulation of xLPB, the first
# recursive call is known, and the only recursive call we may actually need to
# make is the second.
#
# However, this doesn't seem to say anything about the runtime of the second
# recursive call, which seems as though it might cause the evaluation of this
# function to run in time O(n). This is correct, but in an *amortized* sense
# the whole table can still be computed in O(n) time overall. To see this,
# let's define a potential function Phi(k) that associates a potential at each
# point of the computation of the table. In particular, define Phi(k) as
#
# Phi(0) = 0
# Phi(k + 1) = result[k - 1]
#
# Here, result is the resulting table of LPB values. Because of this, we can
# remark that result[k] < k, since the longest proper border of a string can't
# be any longer than that string.
#
# Let's now show that this potential function gives an amortized O(1) cost for
# each table entry computation, and thus an O(n) overall runtime for the table-
# building algorithm. To see this, consider what happens when the logic to
# compute the next value runs. The runtime for this step is bounded by the
# number of recursive calls made to a subproblem. However, each subproblem is
# then of size given by the LPB of a slightly smaller problem. This subproblem
# must then have size at most the size of that smaller subproblem. In other
# words, we can say that each recursive call drops the maximum possible value
# of the LPB for the current prefix by at least one. Consequently, if k
# recursive calls are made, the LPB of the current prefix is at least k smaller
# than the LPB of the previous prefix, and so
#
# D Phi = -k
#
# And so the amortized cost of computing the next term is 1 + k - k = O(1).
def failTable(pattern):
# Create the resulting table, which for length zero is None.
result = [None]
# Iterate across the rest of the characters, filling in the values for the
# rest of the table.
for i in range(0, len(pattern)):
# Keep track of the size of the subproblem we're dealing with, which
# starts off using the first i characters of the string.
j = i
while True:
# If j hits zero, the recursion says that the resulting value is
# zero since we're looking for the LPB of a single-character
# string.
if j == 0:
result.append(0)
break
# Otherwise, if the character one step after the LPB matches the
# next character in the sequence, then we can extend the LPB by one
# character to get an LPB for the whole sequence.
if pattern[result[j]] == pattern[i]:
result.append(result[j] + 1)
break
# Finally, if neither of these hold, then we need to reduce the
# subproblem to the LPB of the LPB.
j = result[j]
return result
# Function: kmpMatch(needle, haystack)
# Usage: print kmpMatch("0101", "0011001011") # Prints 5
# -----------------------------------------------------------------------------
# Uses the KMP algorithm to find an occurrence of the specified needle string
# in the haystack string. To do this, we compute the failure table, which
# is done above. Next, we iterate across the string, keeping track of a
# candidate start point and length matched so far. Whenever a match occurs, we
# update the length of the match we've made. On a failure, we update these
# values by trying to preserve the maximum proper border of the string we were
# able to manage by that point.
def kmpMatch(needle, haystack):
# Compute the failure table for the needle we're looking up.
fail = failTable(needle)
# Keep track of the start index and next match position, both of which
# start at zero since our candidate match is at the beginning and is trying
# to match the first character.
index = 0
match = 0
# Loop until we fall off the string or match.
while index + match < len(haystack):
print index, match
# If the current character matches the expected character, then bump up
# the match index.
if haystack[index + match] == needle[match]:
match = match + 1
# If we completely matched everything, we're done.
if match == len(needle):
return index
# Otherwise, we need to look at the fail table to determine what to do
# next.
else:
# If we couldn't match the first character, then just advance the
# start index. We need to try again.
if match == 0:
index = index + 1
# Otherwise, see how much we need to skip forward before we have
# another feasible match.
else:
index = index + match - fail[match]
match = fail[match]
# If we made it here, then no match was found.
return None
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment