KMP(Knuth–Morris–Prat) 算法是一种能够在 时间内完成字符串匹配的算法,其中 是匹配串的长度, 是模式串的长度。当模式串与匹配串进行匹配,且最新一个字符匹配失败时,朴素算法会丢弃掉所有已经匹配的字符并从头开始,而KMP算法利用 前缀函数 ,充分利用了已经匹配的子串的信息。
KMP算法
首先介绍前缀函数。前缀函数 是这样一个函数:对于串 ,我们记它的前缀 为 ,后缀为 , 就等于使得 成立的最大的 。简单来说,我们取 的前缀 , 就是 最大的前缀与后缀相匹配的长度。
有了前缀函数,当模式串 在串 中进行匹配,且在 的第 项与 的第 项匹配失败( )时,我们就不需要从头开始匹配,因为我们已经知道了 ,利用前缀函数可以得到 ,因此直接从 项开始匹配就行了。
下面是一个示例代码:
std::vector<int> ans;
std::string p,s;
for(int i=0,j=-1;i < s.length();i++){
while(j!=-1 && p[j+1] != s[i])
j=next[j];
if(p[j+1] == s[i])
j++;
if(j == p.length()-1)
ans.push_back(i-s.length()+1);
}
其中, 就是前缀数组,当程序运行结束后, 数组中就存储了 在 中每一次出现的起始位置。
前缀函数的求法
不难发现,求前缀函数的过程其实类似 与自身相匹配的过程。我们可以参照 与 匹配的思路,对于 ,它的前面是 ,如果此时 ,说明 ;否则就沿着 往前跳,直到找到匹配的:
int next[N];
next[0]=-1;
for(int i=1,j=-1;i<p.length();i++)
while(j >= 0 && p[j+1] != p[i])
j=ne[j];
if(p[j+1]==p[i])
j++;
ne[i] = j;
}
时间复杂度分析
看上去,KMP算法使用了二重循环,这是否意味着它的时间复杂度是平方级的呢?并不是。
从上面的代码可以看出,在每一次循环时, 最多增大 ,而每次 都至少使得 减少 。而 始终是一个非负数,因此 for
中的 while
循环执行的总次数不会超过 j++
执行的次数。而 j++
在求前缀函数中最多执行 次,在匹配时最多执行 次,故 while
的次数也不超过这一值。因此,总的时间复杂度为 。
Comments NOTHING