KMP 算法主要是用于解决字符串匹配问题,也就是我们常说的查找子串问题。
术语解释
-
主串(目标串): 简单来说就是被搜索的字符串,一般来说就是那个长的。
-
模式串:被匹配,是查找的目标。
-
算法目标:字符串中的模式定位问题,简单来说就是查找子串,在主串中查找匹配模式串。这一类的算法,又被我们称为模式匹配算法。
说到模式匹配算法,就绕不开暴力,暴力算法的也是有名字的,叫 BF 算法。
BF 算法
BF 算法是一种纯暴力的字符串匹配算法,比较符合人类自然思维方式的方法,即对源字符串和目标字符串逐个字符地进行比较。算法过程,不断的比对,直到在主串中找到目标串或者,遍历完目标串。
例如:S="ABABAC" T="ABA"
匹配过程:
A B A B A C
A B A
匹配失败,继续匹配
A B A B A C
A B A
匹配失败,继续匹配
A B A B A C
A B A
匹配成功,结束
我们发现 BF 算法每次 t 匹配失败都往后移动一个位置,有没有一种可能,在一次匹配中我们能判断移动几次,或者说我们能不能知道向后移动几个是一定不能找到正确答案,然后进行剪枝。
沿着这个思路我们开始考虑一下 KMP 算法。
KMP 算法详解
如果按照正常 BF 算法做字符串匹配的话,我们能够写出如下代码
T = "ABABAC"
P = "ABA"
def BF(tString, pString):
i = 0 #主串中的位置
j = 0 # 模式串中的位置
for i in range(len(tString)-len(pString)+1):
isPatt = True
for j in range(len(pString)):
if tString[i+j]!=pString[j]:
isPatt = False
break
if isPatt:
return i
return -1
print(BF(T, P))
我们看到 i 每次匹配不成功,就回到了初始位置,再向后移动一位。
对于正确性,上面的算法确实是毫无疑问,没有任何问题。但是对于效率上面的算法,确实非常低劣。
而我们将会使用 KMP 算法解决上面这种低效回退的问题。
KMP 算法的核心思想是,利用已经匹配过的这一部分有效的信息,保持主串位置 i 的值不变,去修改模式串的位置 j 的值。
而 KMP 算法就是告诉我们这个 j 该如何去改变。
在讲 Next 数组之前,我们先来讨论一下前后缀的概念。
字符串前后缀:
到这里听不懂的建议直接去背模板,我们以拿奖为最终目标,以了解并且会使用为最终目标即可。
前缀:
符号串左部的任意子串(或者说是字符串的任意首部),在 KMP 算法中使用的是“真”前缀,即不包含自己前缀。
简单记忆方式: 前缀要找除了自己,且从头开始的所有子串。
后缀:
符号串右部的任意子串(或者说是字符串的任意尾部),在 KMP 算法中使用的是“真”后缀,即不包含自己后缀。
简单记忆方式: 后缀要找除了自己,且以最后一个字符结尾的所有子串。
举例:
求 Next 数组前,我们还要去了解一下最长公共前后缀,他的长度对于 KMP 的 Next 数组的计算有着紧密的联系。
最长公共前后缀:
后面的文中将真省略,大家注意。
如果设模式串 P = "abaabca" ,那我们可以得到以某个位置结尾的子串的最长公共前后缀长度。
我们可以得出以下表格:
对于 Next 数组将整体向右移动一位后,在左侧补-1。
Next 数组的含义:
对于模式串"abaabca"而言
出现这种从第 1 位就匹配出错的情况,即使通过人工我们不能找到任何优化方式,于是只能将模式串右移。
恰好模式串的 -1 的位置正好置于主串的 i 位置,这就是 next 的第一位补-1 的原因。
我们再看第二种情况,部分匹配成功的情况。
仍对于模式串"abaabca"而言:
设主串为"abaaefaged",那么会有:
在主串的 i=4 位置,模式串 j=4 的位置发生了不匹配。
我们通过最优人工移动可以得到
恰好与主串 i 位置对应的值是模式串的 1 号位置。
那么与我们计算出的 Next 的数组值相同。
重点来了,我们说一下为什么可以这么神奇!!!
原因:
由于我们通过 next 数组计算,而 next 数组来源于最长公共前后缀的长度,那么为什么最长公共前后缀就能计算出,转移目标呢?
假设某个字符串 s 的最长共前后缀为 X="abcd...",那么这个字符串一定是一下结构,开头是 X 结尾是 X 中间可能会有重叠,匹配到 s 的最后一个字符失败后,那我们知道 X 肯定是匹配成功了,因为 X 不包含最后一个字符。
既然我们知道 X 匹配成功,那么我们一定知道,在主串中一定是从 i 位置开始且一定有一个 X 与模式串中的 X 匹配成功。
如下,点为省略号:
而我们又已知,字符串 s 一定有一个后缀 X,那么我们直接用 s 的后缀 X 去匹配主串的 X,且 X 是最长公共前后缀,那么我们就完成了最优的转移。
当 s 是模式串的从头开始的子串时,就可以得到从某一个字符不匹配时的转移情况。
基本原理已经讲清楚了,我们开始说 KMP 算法。
KMP 算法框架和 BF 大致相似,根据上面的分析,对于字符串的比对我们分为三种。
-
T[i]==P[j] 的情况
此时,两字符相同应该继续比对所以:
i=i+1
j=j+1
-
T[i]!=P[j] 的情况
此时,两字符不相同,j 应该根据 next 数组进行转移,所以:
j=next[j]
-
j=-1 的情况
因为,j=next[j],且 next 第一位为 -1 即出现了第一位就匹配失败的情况,那我们应该做的是,是的模式串的开头向后移动,即:
j=j+1
那么 j 对应 i 的位置也变成了 i+1 所以:
i=i+1
那么与情况 1 相同,我们一同考虑。
**!!!!!!**至此,KMP 算法整体思路完成。
我们可以得到一下 KMP 的模板:
def KMP(sStart, s, p):
# tStart 从主串的哪个位置开始,从头开始为0
# s 主串
# p 模式串
i = sStart # 主串中的位置
j = 0 # 模式串中的位置
sLen = len(s)
pLen = len(p)
while i < sLen and j < pLen:
if j==-1 or s[i] == p[j]:
# 如果j = -1,或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++
i += 1
j += 1
else:
j = next[j]
# 如果j != -1,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = nextt[j]
# next[j]即为j所对应的next值
if j == pLen:
return i - j
else:
return -1
KMP 算法讲解至此结束,但是我们漏下了一个东西,那就是 Next 数组的计算
def GetNext(p):
pLen = len(p)
next[0] = -1
k = -1
j = 0
while j < pLen - 1:
# p[k]表示前缀,p[j]表示后缀
if k == -1 or p[j] == p[k]:
j += 1
k += 1
next[j] = k
else:
k = next[k]