最近学了KMP算法,学了半天终于学会了,感觉还是挺难理解的,在这里与大家分享一下我的思考,也方便我自己复习。
下面就正式进入讲解环节吧(本文所有字符串初始坐标为0!)。
首先我们要明确KMP算法是干什么的:我们有两个字符串T与S,现在我们要找到T中为S的子串,求出满足条件子串数量及位置。这时候我们就要用到KMP算法。
传统算法回顾:
在讲解KMP算法前,我们先看一下传统方式的查询。
在字符串T与S中各设一个指针i,j=1;依次比对i+1,j+1、i+2,j+2……..
如果发现有不同的,则比对i+1,j=1;
用图来看一下(加粗为当前比对位置):
T: c1 c2 c3 c4 c5
S: c1 c2 d3 e4
然后
T: c1 c2 c3 c4 c5
S: c1 c2 d3 e4
然后
T: c1 c2 c3 c4 c5
S: c1 c2 d3 e4
这时我们发现c3与d3不同,就比对T-c2与S-c1,即:
T: c1 c2 c3 c4 c5
S: __ c1 c2 d3 e4
这样的比对,扫描T需要O(n)时间,检查比对需要O(n)时间。
总时间复杂度:O( n^2 ),这样的复杂度还算快的。
然而与KMP相比,还是太慢了,KMP可以把时间复杂度优化到O(n+m);
下面让我们来具体看看KMP的实现。
运用next数组实现跳转:
先引入两个概念:
- 前缀串:例如abcde,则其前缀依次为:a,ab,abc,abcd,abcde
- 后缀串:例如abcde,则其后缀串依次为e,de,cde,bcde,abcde
对于ababa这个目标串(对比串),我们可以发现:
其前缀串与后缀串有相同部分!这就是KMP实现的关键。
比对abacdabe与ababa时,我们可以看到:
T: a b a c d a b e
S: a b a a b a
有第三位置<初始坐标为0>上的a与c不同。
当我们将S往后移动时由于第一位与第三位相同,前两位已经比对过了。
所以后移后绝对不会匹配.
这样就浪费了大量的时间。
而KMP的比对就会直接跳到:
T: a b a c d a b e
S:——a b a a b a
具体是如何实现的呢?我们引入一个F[i]来记录目标串S的前缀串与后缀串的最大相同量。
例如:a b a a b a
则对于未知i,有:
i=1,a,F[1]=无;
i=2,a b,F[2]=无;
i=3,a b a,F[3]=1;
i=4,a b a a,F[4]=1;
i=5,a b a a b,F[5]=2;
i=6,a b a a b a,F[6]=3;
但为了方便计数(等会你就知道为什么了),也方便区分无相同前缀的位置,我们将所有F减1,即无相同前缀的为-1;
所以:
F[1]=-1;F[2]=-1;F[3]=0;F[4]=0;F[5]=-1;F[6]=2;
下文中所有的表示都是这样的。
然后我们用几个例子看一看next是如何实现跳转的。
例如匹配abaaddddd与abaaba,则用前缀数组的方法为:
前缀数组:a -b a a -b a(‘-‘方便对齐)
———— -1 -1 0 0 1 2
匹配到:
T: a b a a d d d d d
S: a b a a b a
F[5-1]=0,有一个相同的前缀,j直接跳转到F[j-1]+1=1(注意初始下标为0);即:
T: a b a a d d d d d
S: ——–a b a a b a
此时匹配位置前一个的’a’已经匹配,实现跳转。再看一个:
匹配abaabdddd与abaaba,匹配到:
T: a b a a b d d d d
S: a b a a b a
此时F[6-1]=1,有1+1=2个相同前缀,j跳转到F[j-1]+1=2,即:
T: a b a a b d d d d
S: ——–a b a a b a
读者们可以仔细体会一下上面两个例子,这里我给出跳转的总结:
if(t[i]!=s[j])
{
if(j==0)i++; //没有相同前缀,直接向后移(与一般方法相同)
else j=F[j-1]+1; //跳转,KMP
}
求解next数组:
知道如何用next数组实现跳转后,我们还要想办法实现next数组的求解。
对于next数组,我们求解的方法与上文其实差不多,但是用到了一个思想:自己匹配自己!
我们先思考:求解next数组肯定要用O(m)的算法吧。(要保证KMP的时间复杂度)。
这里的思路是一个递推思路。
结合下面的代码读者们不妨先想一想,什么样的情况下F[i]可以由前面的F[k]加1求得。
先给出求解F数组的代码:
void get_low()
{
len=t.length();
F[0]=-1;
int temp=0,i;
for(i=1;i<=len-1;i++) //初始下标为0!
{
int j=F[i-1];
while(t[j+1]!=t[i]&&j>=0)j=F[j];
if(t[j+1]==t[i])F[i]=j+1;
else F[i]=-1;
}return;
}
还是先从例子开始吧。
所求前缀数组:
S: a _b a a b _b a b a a b
F: -1 -1 0 0 1 -1 0 1 2 3 4
我们求解第二个:
S:a __b a a b
F:-1 -1
此时这个a的前一个b的F值为-1,所以此时的a不能由b的F[1]求出,j=-1;
比对S[j+1]=a,与当前位a相同,所以F[i]=F[2]=j+1=0;
我们求解第五个:
S= a b a a b b
F=-1 -1 0 0
此时j=F[i-1]=F[4]=0,但当前位置b与前一个a不同,无法接上,j=F[j]=0;
然后比对S[j+1]=S[2]=’b’与当前位b相同,F[i]=F[5]=j+1=1;
这一部分虽然代码很短,但这是KMP的难点,比较难理解(但一定不能背代 码),希望读者们进行一次完整的模拟以助于理解(对照上文的代码),这里给出一个典型的例子以供参考:
–典型例子:S: a _b a a b _b a b a a b //下标线为方便对齐
–对应next:F:-1 -1 0 0 1 -1 0 1 2 3 4
这个例子把next的所有情况都包含到了,希望读者们一定要耐着性子模拟一遍。
理解了以上内容后(我承认有点难度),KMP算法的实现应该也就不难了,下面我给出完整的代码<内含用next匹配的具体代码>:
模板题来自于:洛谷 P3375 【模板】KMP字符串匹配
题目描述
如题,给出两个字符串s1和s2,其中s2为s1的子串,求出s2在s1中所有出现的位置。
为了减少骗分的情况,接下来还要输出子串的前缀数组next。
(如果你不知道这是什么意思也不要问,去百度搜[kmp算法]学习一下就知道了。)
输入输出格式
输入格式:
第一行为一个字符串,即为s1(仅包含大写字母)
第二行为一个字符串,即为s2(仅包含大写字母)
输出格式:
若干行,每行包含一个整数,表示s2在s1中出现的位置
接下来1行,包括length(s2)个整数,表示前缀数组next[i]的值。
输入输出样例
输入样例#1:
ABABABC
ABA
输出样例#1:
1
3
0 0 1
说明
时空限制:1000ms,128M
设s1长度为N,s2长度为M
对于100%的数据:N<=1000000,M<=1000
完整代码(即KMP算法的模板):
#include<cstdio>
#include<cstdlib>
#include<iostream>
#include<algorithm>
#include<cstring>
#include<cmath>
using namespace std;
char t[10000002],s[10002]; //原串、模式串
int F[10002]; //前缀next数组
int len,len1;
inline void get_bef() //求解next前缀数组
{
int i,j;
F[0]=-1;len=strlen(s);
for(i=1;i<=len-1;i++)
{
j=F[i-1];
while(j>=0 && s[i]!=s[j+1])j=F[j];
if(s[i]!=s[j+1])F[i]=-1;
else F[i]=j+1;
}return;
}
inline void find_pos() //求解位置
{
int i,j;
len1=strlen(t);j=0;i=0;
while(i<=len1-1)
{
if(t[i]==s[j]) //这里为正常匹配
{
i++;
j++;
if(j==len)printf("%d\n",i-j+1);
}
else //KMP跳转
{
if(j==0)i++;
else j=F[j-1]+1;
}
}return;
}
int main()
{
scanf("%s",&t);
scanf("%s",&s);
get_bef();
find_pos();
for(int i=0;i<=len-1;i++)printf("%d ",F[i]+1);
return 0;
}
希望我的一点理解感悟能够帮助到oier们,呵呵呵,谢谢观看。