一些简单概念的定义
- 子串:字符串中连续的一段
- 前缀:出现在字符串开始位置的子串
- 后缀:出现在字符串结束位置的子串
- 真前缀:除字符串本身外的所有前缀
- 真后缀:除字符串本身外的所有后缀
给定一个字符串T(文本) 和一个单词P, 问P在T中出现过几次?分别在哪几个位置出现?
一些约定:
- n和m: 文本T的长度、P的长度
- ?_?: 字符串P的第i个字符(下标从1开始)
- ?[?…?]: 字符串P的第l个字符到第r个字符组成的字符串
朴素暴力解法
枚举T中的每个位置,检查P是否在此出现
最差时间复杂度为?(??)
kmp算法
KMP算法的主要思想是利用以比较结果跳过一些无效比较,是一种解决字符串匹配问题的线性时间算法
使用了一个辅助数组F,F定义如下:
- F[i]是一个的比i小的,最大的,满足 P[1…F[i]] 是P[1…i]的一个真后缀的数;
即每个F[i]都是P[1…i]的border(一个字符串中与真后缀相同的真前缀的最大长度)
F[i]代表最大的比i小的数,并且满足 P[1…i] 是P的一个真后缀
利用F数组快速匹配
代码实现
void getMatch(char *P,int m,char *T,int n)
{
getF(P,m);
int k=0;
for(int i = 1; i <= n; ++i)
{
while(k&&T[i]!=P[k+1]) k=F[k];
if(T[i]==P[k+1]) ++k;
if(k==m)
{
printf("matched at %d\n", i-m+1);
k=F[k];
}
}
}
如何求出F数组
朴素求法:
枚举P[1…i]的前缀,check是否是P[1…i]的后缀,最长的满足条件的前缀即为F[i]
最差时间复杂度为?(?^? )
观察一下:
如果P[1…F[i]]是P[1…i]的一个后缀,那么P[1…F[i]-1]一定是P[1…i-1]的一个后缀;
再观察一下:
令Q=P[1…i]
P[1…F[i]]是Q的后缀
P[1…F[F[i]]]是Q的后缀
P[1…F[F[F[i]]]]是Q的后缀
P[1…F[F[F[F[i]]]]]是Q的后缀
P[1…F[F[F[F[F[i]]]]]]是Q的后缀
不断的把i带入F[i]得到的字符串P[1…F[i]]都是P的一个后缀
代码实现
void getF(char *P,int m) {
F[1]=0;
for(int i = 2; i <= m; ++i)
{
int k=F[i-1];
while(k&&P[k+1]!=P[i]) k=F[k];
if(P[k+1]==P[i]) ++k;
F[i]=k;
}
}
挖坑