首先我们来看一下教材(教材讲的非常高大上不理解也没关系,继续往下看就行了):
然后教材还给了一个例题:
如果看不懂上面的内容的话,从这开始往下看也没问题哦~
其实这个例题一句句的看,结合上面的公式还是勉强能理解的,只不过解题过程可能会比较慢,总的来说匹配的关键就是移位,移位的关键就是上面那个表里面next[i]里面的值。
next[j]=k,含义是:下标为j 的字符前的字符串最长相等前后缀的长度为k。 我认为理解这个是比较关键的,接下来我来解释下什么是最长相等前后缀。
比如有个字符串是 abcdab
所谓的前缀集合我们就可以理解为去掉最后一个字符,然后从第一个字符往后迭代,每一次迭代时的长度增加1然后存到集合里面,后缀集合相反,于是有了下面这个结果
前缀的集合:{a,ab,abc,abcd,abcda}
后缀的集合:{b,ab,dab,cdab,bcdab}
通过对比两个集合,可以看到相同的部分为ab,长度是2,所以最长相当前后缀长度就是2
再次回到上面不好理解的例题,子串是abababb,我们随便算一个j=4的情况吧,next[4]对应的第五个字母也就是a,我们刚才说了:next[i]=j,含义是:下标为i 的字符前的字符串最长相等前后缀的长度为j。这个“前”字很重要,所以我们需要的是第五个之前的四个字母,也就是abab,于是乎:
前缀的集合:{a,ab,aba}
后缀的集合:{b,ab,bab}
可以得到相等的部分是ab,长度是2,所以上面的next[4]对应的值是2,似不似赶赶单单。
那么这个时候编教材的人不乐意了,“劳资辛辛苦苦给你们弄个公式,你们却用这种方法投机取巧”,那么接下来我们再用公式算一下这个next[4]:
子串abababb
根据公式,当j=4的时候,k的值可以为1,2,3,我们先用大的值进行测试(因为求得是最长相等前后缀,大值满足条件就不用管小的了),看看其满不满足后面的条件
下面分析里面的P0代表子串的第一个字母,P1代表第二个,以此类推:
当k=3时,等式左边为P0P1P2=aba 等式右边为P1P2P3=bab 左边不等于右边,等式不成立
当k=2时,等式左边P0P1=ab 等式右边P2P3=ab 左边等于右边,等式成立,所以next[4]=2
相关代码及改进代码(参考其它KMP算法文章):
typedef struct
{
char data[MaxSize];
int length; //串长
} SqString;
//求next数组
//SqString 是串的数据结构
//typedef重命名结构体变量,可以用SqString t定义一个结构体。
void GetNext(SqString t,int next[]) //由模式串t求出next值
{
int j,k;
j=0;k=-1;
next[0]=-1;//第一个字符前无字符串,给值-1
while (j<t.length-1)
//因为next数组中j最大为t.length-1,而每一步next数组赋值都是在j++之后
//所以最后一次经过while循环时j为t.length-2
{
if (k==-1 || t.data[j]==t.data[k]) //k为-1或比较的字符相等时
{
j++;k++;
next[j]=k;
//对应字符匹配情况下,s与t指向同步后移
//通过字符串"aaaaab"求next数组过程想一下这一步的意义
//printf("(1) j=%d,k=%d,next[%d]=%d\n",j,k,j,k);
}
else
{
k=next[k];
**//我们现在知道next[k]的值代表的是下标为k的字符前面的字符串最长相等前后缀的长度
//也表示该处字符不匹配时应该回溯到的字符的下标
//这个值给k后又进行while循环判断,此时t.data[k]即指最长相等前缀后一个字符**
//为什么要回退此处进行比较,我们往下接着看。其实原理和上面介绍的KMP原理差不多
//printf("(2) k=%d\n",k);
}
}
}
int KMPIndex(SqString s,SqString t) //KMP算法
{
int next[MaxSize],i=0,j=0;
GetNext(t,next);
while (i<s.length && j<t.length)
{
if (j==-1 || s.data[i]==t.data[j])
{
i++;j++; //i,j各增1
}
else j=next[j]; //i不变,j后退,现在知道为什么这样让子串回退了吧
}
if (j>=t.length)
return(i-t.length); //返回匹配模式串的首字符下标
else
return(-1); //返回不匹配标志
}
优化代码
void GetNextval(SqString t,int nextval[])
//由模式串t求出nextval值
{
int j=0,k=-1;
nextval[0]=-1;
while (j<t.length)
{
if (k==-1 || t.data[j]==t.data[k])
{
j++;k++;
if (t.data[j]!=t.data[k])
//这里的t.data[k]是t.data[j]处字符不匹配而会回溯到的字符
//为什么?因为没有这处if判断的话,此处代码是next[j]=k;
//next[j]不就是t.data[j]不匹配时应该回溯到的字符位置嘛
nextval[j]=k;
else
nextval[j]=nextval[k];
//这一个代码含义是不是呼之欲出了?
//此时nextval[j]的值就是就是t.data[j]不匹配时应该回溯到的字符的nextval值
//用较为粗鄙语言表诉:即字符不匹配时回溯两层后对应的字符下标
}
else k=nextval[k];
}
}
int KMPIndex1(SqString s,SqString t)
//修正的KMP算法
//只是next换成了nextval
{
int nextval[MaxSize],i=0,j=0;
GetNextval(t,nextval);
while (i<s.length && j<t.length)
{
if (j==-1 || s.data[i]==t.data[j])
{
i++;j++;
}
else j=nextval[j];
}
if (j>=t.length)
return(i-t.length);
else
return(-1);
}