KMP(快速模式串匹配)
概述
KMP算法是一种高效的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出,所以称作KMP算法 快(K)速模(M)式串匹(P)配
理解
要求模式串在主串中的匹配次数,直接暴力枚举每两个位置是否一样,时间复杂度 \(\Theta(nm)\),显然无法接受,那么怎么优化呢?
暴力算法中每次两个指针分别指向两个字符串,一旦不匹配,主串指针回滚到开始匹配的字符的下一个,模式串指针回滚到开头,但是真的有必要吗?我们已经确认了两个指针所指向的字符第一次不匹配的地方前面的所有字符都是相同的,那么当遇到不匹配的时候能不能不从头开始,而是从某个特定的地方开始来让匹配更快呢?
例如图中的这种情况,此时发现主串与模式串不匹配,暴力算法会将 \(i\) 回滚到第二个字符,将 \(j\) 指向模式串开头来重新匹配,但是这是并不必要的,因为我们已经知道了主串的 \(i\) 前的 \(4\) 个字符和模式串的 \(j\) 前的 \(4\) 个字符是相等的,这时只要把 \(j\) 回到指向第三个字符,\(i\) 不动即可,减少了很多匹配次数,如下图
为了实现这一操作,KMP算法引入了一个新的数组 \(next\),它存储着模式串匹配到某个位置不匹配时应该回退到哪个地方, \(next\) 数组的具体含义就是:\(next_i\) 表示在模式串中前 \(i\) 个字符组成的字符串的最长公共前后缀长度,也就是前 \(i\) 个字符组成的字符串的前缀和后缀的交集中长度最长的那一个
例如字符串 \(cmcmy\),它的前缀有 \(c,cm,cmc,cmcm\)(注意一个字符串的前后缀均不包括它自身),后缀有 \(y,my,cmy,mcmy\),它们的交集为空集,于是乎 \(next_5=0\),它的前四个字符的前缀有 \(c,cm,cmc\),后缀有 \(m,cm,mcm\) 交集有 \(cm\),最长的长度为 \(2\),所以 \(next_4=2\)
当遇到不匹配时,根据 \(next\) 数组移动两个指针就能节省很多不必要的操作
过程
next数组预处理
预处理 \(next\) 数组就是计算前缀函数,关于前缀函数计算的优化过程这里就不多赘述,直接给出终极版本,感兴趣的读者可以去查阅相关资料
预处理 \(next\) 数组其实就是 \(next\) 数组和自己匹配,将子串的前缀后缀对齐,看一样的最长有多长就行
显然我们知道 \(next_1=0\)
我们使用一个指针 \(i\) 指向模式串第 \(i\) 位,从第 \(2\) 位开始计算,表示当前正在求算 \(next_i\),显然 \(next_i\) 至多比 \(next_{i-1}\) 大 \(1\),如果 \(next_i=next_{i-1}+1\) 就说明此时的最长公共前缀比上次的增加了 \(1\),那么就说明模式串的第 \(i\) 位与第 \(next_{i-1}+1\) 位相等,所以我们可以说如果模式串的第 \(i\) 位与第 \(next_{i-1}+1\) 位相等就有 \(next_i=next_{i-1}+1\),如果不相等那么我们就继续计算模式串的第 \(i\) 位是否与第 \(next_{next_{i-1}}+1\) 位相等,以此类推,直到找到相等或是 \(next\) 数组中的对应值为 \(0\),如果是 \(next\) 数组中的对应值为 \(0\),那么 \(next_i=0\)
c++ code
char p[MAXN],nxt[MAXN];
void pre_work()
{
int plen=strlen(p+1);////获取字符串长度
for(int i=2,j=0;i<=plen;i++)//枚举i,而j从next[1]即0开始
{
//此时j仍然等于上一轮循环的next[i-1]
while(j&&p[i]!=p[j+1]) j=nxt[j];//只要不相等或者没到0就继续往回跳
if(p[i]==p[j+1]) nxt[i]=++j;//相等就把j向后挪一位并赋值给next
else nxt[i]=0;//不相等就是没有匹配,next=0
}
}
匹配
每次一旦在 \(j\) 处不匹配,只要 \(j\ne0\) 就将 \(j\) 移动到 \(next_j+1\) 再判断,否则就把 \(i\) 移动到 \(i+1\),这样就能一直匹配下去
c++ code
char s[MAXN],p[MAXN],nxt[MAXN];
void kmp()
{
int slen=strlen(s+1),plen=strlen(p+1);//获取字符串长度
for(int i=1,j=0;i<=slen;i++)//枚举主串指针,j从next[1]也就是0开始
{
while(j&&p[j+1]!=s[i]) j=nxt[j];//如果不匹配就往前找直到匹配或者到模式串开头
if(s[i]==p[j+1]) j++;//匹配成功就往后多挪一位
if(j==plen)//如果整个模式串都匹配成功
{
printf("%d\n",i-j+1);//输出成功匹配起点的位置
j=nxt[j];//继续往前跳
}
}
}
总结
以上就是KMP算法的大致内容,以前学习KMP算法花费了很多时间,现在写一篇blog算是彻底理解了这种算法,如果有哪里有写的不清楚或者是错误欢迎在评论区提出,我将会尽快修改,希望大家共同进步!
感谢
该文为本人原创,转载请注明出处