参考题目:【模板】KMP字符串匹配
(题目里的Next数组和我的Next数组略有差别)
数据结构讲到串时老师把KMP给详细讲了一遍,之前两次尝试自学实在是搞不明白,这下老师讲解了思路后差不多明白了,把自己的理解写一下,以后自己又忘了的时候说不定回来康康就懂了
KMP算法
//先假定主串为S、模式串为T
BF算法就是暴力匹配,如果当前T串的第j个字符与S串的第i个字符不匹配,那么j回到1,i回到i-j+2。这种方法一定能匹配到,但是速度慢。
KMP算法是一种可以使i不回溯的算法,因此达到了线性复杂度。
先看概念
给定串:ABCABA
前缀:A,AB,ABC,ABCA,ABCAB,ABCABA
后缀:A,BA,ABA,CABA,BCABA,ABCABA
然后康KMP算法的大体思路
假设我们已经用一个数组next记录了模式串当前位置j的回溯位置。
可以看到此时S[i]!=T[j],但是之前的部分都是匹配成功的。此时假设T1…Tk-1的前缀A和后缀B是相同的。我们已经知道B和C已经匹配成功了,那么A和C也能成功匹配,因此j不再需要回到模式串的头,只需要回到前缀A的下一个位置即可。因此令j=next[j],继续匹配操作。
先上一手主题函数的代码
//为了偷懒直接用了string
void KMP(string S, string target)
{
//初始化
int i = 1; int j = 1;
int* next = GetNext(target);
while (i < S.length())
{
//如果j==0说明模式串第一个字符就不匹配,主串+1
//如果当前字符匹配了都前进一位
if (!j || S[i] == target[j]) { i++; j++; }
else
{
j = next[j];//回溯
}
if (j >= target.length())
{
//在OJ中用cout会TLE
//因为题目是要一直匹配到主串最后一个字符,所以模式串匹配结束还要回溯
printf("%d\n", i - target.length() + 1);
j = next[j];
}
}
free(next);
}
那么问题来了!
这个next数组咋求?
这也是我之前两次自学没搞懂的地方。首先,不难看出每个next[j]都记录着T1…Tk-1中最长的相等的前缀后缀长度+1(ST都从1开始储存数据)。
首先next[1]=0是初始化设置,因为如果模式串第一位都不匹配说明主串需要再往后走一位了,其次next[2]=1也是初始化,因为前面只有1个字符,若不匹配直接回到串头。
当j>2之后,next[j]就是表示最长长度+1。如j==7的c,前面相同的前后缀是ab,因此回溯时直接跳到T[3]。
虽然我们可以一遍一遍遍历来实现next数组,但是那样复杂度又变成平方级别了,所以如果能用KMP算法让模式串匹配自己,就能提高效率。
上求next数组的代码:
重要程度:五颗心♥♥♥♥♥
//可以注意到和KMP的结构一毛一样
int *GetNext(string t)
{
int *next = (int*)malloc(sizeof(int)*(t.length() + 3));//随便分配了个长度,比模式串稍微长一点就行
next[1] = 0;
int i = 1;
int k = 0;//k的含义是到目前为止前k个字符得到匹配,相当于kmp里的j
for (; i < t.length(); )
//这里注意,因为当模式串成功匹配之后还需要回溯进行下一轮匹配
//所以我们得知道当模式串最后一个字符匹配成功后应该跳回哪里
//所以next[l.length](最后一个字符的下一个位置)也需要存值
{
if (k == 0 || t[i] == t[k]) {i++; k++;}
else k = next[k];
}
return next;
}
假设你已经知道了next[j]=k,这说明前k个已经相等,那么现在有两种情况
①假如T[j]=T[k],那么说明长度为k+1的前缀和后缀再次相同
这时候让next[j+1]=k+1即可
②假如不相等。这时我们需要把模式串想象成主串在这里插入图片描述在这里插入图片描述
从左到右红色斜线阴影分别为ABC
假设next[k]=k’,说明A=B,又因为next[j]=k,因此B=C,所以A=C。此时再次比较T[k’]和T[j],如果相等则next[j+1]=k‘+1反之重复②号步骤,直到k=0。若k=0,则让next[j]=1(因为没有相同子串,所以回溯到串头)。
next数组的修正
改进后代码:
int *GetNext(string t)
{
int *next = (int*)malloc(sizeof(int)*(t.length() + 3));
next[1] = 0;
int i = 1;
int k = 0;//k的含义是到目前为止前k个字符得到匹配
for (; i < t.length(); )
{
if (k == 0 || t[i] == t[k])
{
i++; k++;
//如果回溯后的元素相同,那么继续回溯
if(t[i]!=t[k])
{
next[i] = k;
}
else next[i] = next[k];
}
else k = next[k];
}
return next;
}
洛谷的题的next数组和我的next数组的区别在于,他的next[j]存的是j位置相同前缀的最后一个元素位置,但是实际上思路是一样的。