KMP算法(推导方法及模板)
介绍
克努斯-莫里斯-普拉特算法Knuth-Morris-Pratt字符串查找算法(简称为KMP算法)可在一个主文本字符串S
内查找一个词W
的出现位置。此算法通过运用对这个词在不匹配时本身就包含足够的信息来确定下一个匹配将在哪里开始的发现,从而避免重新检查先前匹配的字符。
此算法可以在O(n+m)时间数量级上完成串的模式匹配操作,其改进在于:每当一趟匹配过程中出现字符比较不等时,不需回溯i的指针,而是利用已经得到的“部分匹配”的结果将模式向右“滑动”尽可能远的距离后,继续进行比较。
kmp的核心之处在于next数组,而为了方便理解,我先介绍KMP的思想
KMP匹配
当开始匹配时,如果匹配过程中产生“失配”时,指针i(原串的下标)不变,指针j(模式串的下标)退回到next[j] 所指示的位置上重新进行比较,并且当指针j退回至零时,指针i和指针j需同时加一。即主串的第i个字符和模式的第一个字符不等时,应从主串的第i+1个字符起重新进行匹配。
简单来说,就是两个串匹配,如果当前字符相等就比较两个字符串的下一个字符,如果当前匹配不相等时,就让j(待匹配串的下标)回到next[j] 的位置,因为我们已经知道next数组的作用是利用已经得到的“部分匹配”的结果将模式向右“滑动”尽可能远的距离,如ababac与abac比较时i=4,j=4时不匹配,则利用next数组让j=2继续匹配而不用重新开始。(目前先不用管next数组的值时如何得到的,只要明白它的作用即可,下面回介绍)
所以我们可以写出kmp的代码
int KMP(char str[],char pat[])
{
int lenstr=strlen(str);
int lenpat=strlen(pat);
int i=1,j=1;
while(i<=lenstr)
{
if(j==0 || str[i]==pat[j]) //匹配成功继续往后匹配
++i,++j;
else
j=next[j]; //否则根据next数组继续匹配
if(j==lenpat) //说明匹配完成
return 1;
}
return 0;
}
接下来就是关键的求next数组了
next数组
首先,next数组取决于模式串本身而与相匹配的主串无关,我们可以对其递推得到。
网上讲next数组求解的博客一大堆,我就不那样从定义那一扯一大堆了,随便说一下如何推算的吧。
举个例子,如abacabc,首先,next[ 1 ]=0(下标从1开始),后面next[j]的值就看第j-1个字符是否与前面的匹配,如果匹配next[j]=next[j-1]+1,否则其他情况next[j]=1。这样说太笼统,我们看例子:
next[2]=1(第一个字符无法匹配,所以为1),
next[3]=1(第二个字符与第一个字符不相等),
next[4]=2(第三个字符与第一个字符相匹配,所以就等于next[3]+1),
next[5]=1(因为第4个字符与前面没有匹配的),
next[6]=2(同样第5个字符与第1个字符匹配),
next[7]=3(因为第6个字符b与第2个字符b匹配,同时由next[6]可知第5个字符与第1个字符同样匹配,即子串ab与ab匹配,故next[7]=next[6]+1)
下面给出几个例子,可以自己推导一下
abcdex
011111
abcabx
011123
ababaaaba
011234223
aaaaaaaab
012345678
如果理解了推导过程的话再回头看代码就好理解了,就算不理解也不要紧,先用着,过一段时间再去消化,下面是代码:
void getnext(char *pat)
{
int i=1,j=0;
int len=strlen(pat);
next[1]=0;
while(i<len)
{
if(j==0 || pat[i]==pat[j])
{
++i;
++j;
next[i]=j;
}
else
j=next[j];
}
优化
前面定义的next在某些情况下有缺陷,如模式串aaaab和主串aaabaaaab匹配时,仍有许多不必要的步骤,所以下面代码在这种情况做了优化:
void getnext(char *pat)
{
int i=1,j=0;
int len=strlen(pat);
next[1]=0;
while(i<len)
{
if(j==0 || pat[i]==pat[j])
{
++i;
++j;
if(pat[i]!=pat[j])
next[i]=j;
else
next[i]=next[j];
}
else
j=next[j];
}
模板
而下面是常用的模板,可以找到匹配下标与匹配次数,在与前面的略有些不同,其实就是next的值都减1罢了。
#include <iostream>
#include<cstring>
#include<cstdio>
using namespace std;
char str[1000010],pat[1000010];//pat为模式串,str为主串
int Next[1000010]; //Next[x]下标x表示匹配失败处字符下标
//模式串pat的前缀与x位置的后缀的最大匹配字符个数-1
void GetNext(char *pat)
{
int LenPat = strlen(pat);
int i = 0,j = -1;
Next[0] = -1;
while(i < LenPat)
{
if(j == -1 || pat[i] == pat[j])
{
i++,j++;
Next[i] = j;
}
else
j = Next[j];
}
}
int KMP()//返回模式串pat在str中第一次出现的位置
{
int LenStr = strlen(str);
int LenPat = strlen(pat);
GetNext(pat);
int i = 0,j = 0;
int ans = 0;//计算模式串在主串匹配次数
while(i < LenStr)
{
if(j == -1 || str[i] == pat[j])
i++,j++;
else
j = Next[j];
if(j == LenPat)
{
//ans++; ans存放匹配次数,去掉return,最后返回ans
return i - LenPat + 1;
}
}
return -1;//没找到匹配位置
//return ans;//返回匹配次数。
}
int main()
{
scanf("%s%s",str,pat);
int i=KMP();
printf("%d\n",i);
return 0;
}