【算法理解】
这个程序的算法是相当“朴素”的:变量k是在字符串s中搜索t的起点,用指针i与j分别在s与t中扫描,逐一比较s[i]与t[j]是否相同。若遇到不相同的情况,则将起点k在s中后移一个字符,继续搜索。客观地讲,在两个字符串都不很长的情况下,这个算法的执行效率还是可以的。
但在某些特殊情况下,这个算法的效率问题就会显现出来,它最大的问题就在于:当出现s[i]!=t[j]时,指针i要回到前面的字符重复比较。
比如这种情况: s: ababcabcacbab t: abcac
当起点k=2时,如表-1所示
|
|
| k,i |
|
|
|
|
|
|
|
|
|
|
s | a | b | a | b | c | a | b | c | a | c | b | a | b |
t |
|
| a | b | c | a | c |
|
|
|
|
|
|
|
|
| j |
|
|
|
|
|
|
|
|
|
|
i从k(即2)开始,j从0开始比较,前四对字符都是比较成功的(表-1中的灰色部分),只在最后一个字符的比较时出现了问题,见表-2:
|
|
| k |
|
|
| i |
|
|
|
|
|
|
s | a | b | a | b | c | a | b | c | a | c | b | a | b |
t |
|
| a | b | c | a | c |
|
|
|
|
|
|
|
|
|
|
|
|
| j |
|
|
|
|
|
|
KMP算法对前述算法的改进之处就是变量i不需回复。
还从表-2分析,此时出现了s[i]!=t[j]的情况,我们的问题是:若变量i原地不动,它应该和t的哪一个字符继续进行比较呢(即变量j的值应修改为多少)?
在挑选j值时需要保证:新的t[j]之前的部分应该是与字符串s匹配成功的,通过观察,发现当j=1时,可以满足要求。
|
|
|
|
|
|
| i |
|
|
|
|
|
|
s | a | b | a | b | c | a | b | c | a | c | b | a | b |
t |
|
|
|
|
| a | b | c | a | c |
|
|
|
|
|
|
|
|
|
| j |
|
|
|
|
|
|
此时,t[j]前面的部分与s[i]前面的等长部分是完全相同的,因此可以“放心大胆”地从此时的s[i]与t[j]开始,继续比较下去。
下面分析当s[i]!=t[j]时,求下一个j的算法。
分析表-2与表-3的t一行,发现表-3中j之前的部分,恰好是表-2中j之前的部分的后缀,而t这一行标明的都是字符串t的内容,表-3不过是将t的位置后移了,因此我们也可以说:表-3中j之前的部分,也是表-2中j之前的部分的前缀。
所以,若有s[i]!=t[j],找下一个j的方法是:在字符串t里j之前的部分中,找到即是前缀又是后缀的那个字符串(且要最长的那个)——假定它的长度为k,那么下一个j就取k(因为C++数组下标从0开始)。同时也得到另外一条结论:若有s[i]!=t[j],下一个j的选择只依赖于字符串t本身的性质,与字符串s无关,且这个值是应该固定的。
定义整数数组n[],n[j]存储当s[i]!=t[j]时,j的下一取值。
我们用递推的方法确定数组n[]。
首先在n[0]处设置监视哨,规定n[0]= -1(即字符串最小可用下标0的前一个数字,在后面的讨论中将看到这一设置的好处:它可以将原本的三种情况,归结为两种情况)。
然后用递推方法确定其它值,假定n[0]~n[j]的值都已经确定了,现由已知的这些数据来确定n[j+1]的值。
|
|
|
|
| j | j+1 |
|
|
|
|
|
|
|
n | -1 | 0 | 0 | 0 | 1 |
|
|
|
|
|
|
|
|
t | a | b | c | a | b | d | d |
|
|
|
|
|
|
t |
|
|
| a | b | c | a | b | d | d |
|
|
|
|
|
|
|
| k |
|
|
|
|
|
|
|
|
这只是一种情况,下面讨论另一种t[j]!=t[k]的情况,看表-5。
|
|
|
|
|
| j |
|
|
|
|
|
|
|
n | -1 | 0 | 0 | 0 | 1 | 2 |
|
|
|
|
|
|
|
t | a | b | c | a | b | d | d |
|
|
|
|
|
|
t |
|
|
| a | b | c | a | b | d | d |
|
|
|
|
|
|
|
|
| k |
|
|
|
|
|
|
|
此时已经确定n[0]~n[5]的值,现确定n[6]。已经有t[j]!=t[k],j+1之前的即是前缀又是后缀的最长子串已经不能由简单的由“延长”来得到了,但我们知道另一个有用的信息,即:若在t[k]处出现字符不相同,我们应该将k的值变为n[k],由于k<j,因此这个n[k]是已经确定的。通过这种方法不停缩小k值,直到出现两种情况:(1)若t[k]= =t[j],可以参考前一种方案;(2)若k= = -1,说明找不到合适的子串,要从t[0]开始比较,此时就可以用到监视哨了——n[j+1]=k+1。
当有了数组n[]之后,在s中搜索t时,若有s[i]!=t[j],则取j=n[j]继续比较就是了,若出现j<0或s[i]= =t[j]的情况,i、j都后移。
【算法模板】
#include<bits/stdc++.h>
using namespace std;
char t[1001],s[1001];
int n[1001],m;
void next(){
int i=0,k=-1;
n[0]=k;
while(i<m)
if(k<0||t[k]==t[i]) n[i++]=++k;
else k=n[k];
}
int match(){
int i,j;
while(i<int(strlen(s))&&j<int(strlen(t)))
if(j<0||s[i]==t[j]){
i++;
j++;
}
else j=n[j];
if(j==strlen(t)) return i-j+1;
else return -1;
}
int main()
{
cin>>s>>t;
m=strlen(t);
next();
cout<<match()<<endl;
return 0;
}