什么是KMP
由Knuth,Morris,pratt发明的一种模式匹配算法,可以大大避免重复遍历的情况,成为KMP算法。
前缀表
前缀: 包含首字母不包含尾字母的所有子串。
后缀: 包含尾字母不包含首字母的所有字串。
为什么要介绍前缀表和前缀呢?事实上,next数组就是一个前缀表。
前缀表的作用: 用来回退,它记录了当模式串与主串不匹配的时候,模式串应该重新匹配的位置。
前缀表: 记录下标i之前(包括i)的字符串中,最大相等前后缀的长度。
最大相同前后缀: 长度最长且前后缀相同的子串。
如何计算前缀表
对于模式串 a a b a a f
"a" 最长相同前后缀长度为0(只有一个字符,而满足有前缀后缀至少需要两个字符)。
"a a" ----------------为1.
"a a b"---------------为0.(尾字母为b,除了尾字母没有b,前后缀不可能相同)。
同理,"a a b a" , "a a b a a" , "a a b a a f"最长相同前后缀长度分别为1,2,0.所以该模式串的前缀表为
计算出前缀表之后,观察字符串下标与相应前缀表对应位置的数字大小的关系:下标i(包括i)的字符串中最大相同前后缀的长度。
如何应用前缀表找到回退位置
当匹配的字符不同时,前一个字符的前缀表的数值,回退到那个位置重新开始匹配。
如上图所示,模式串与文本串匹配到f时不同,模式串指针回退到下标2(前一个字符对应的前缀表的值)处重新开始匹配。
前缀表与next数组
前面介绍到前缀表可以就是说时next数组,但next数组既可以是前缀表,也可以是前缀表-1,原理相同,只是实现的细节不一样。
构造next数组
构造next数组分为3步。
1.初始化
定义两个指针i(指向后缀起始位置),j(指向前缀起始位置)。再对next数组进行初始化赋值。
int j = -1; //初始化为-1是前缀表统一减一的结果。
next[0] = j;
2.处理前后缀不相同的情况
因为j初始化为-1,令i=1,开始s[i]与s[j+1]的比较。(此时j+1==0)从i=1开始遍历,遇到s[i]与s[j+1]不相同的情况,即前后缀末尾不相同时,就要向前回退。
for(int i = 1;i<len;i++)
{
while(j>=0 && s[i] != s[j+1])
{
j = next[j]; //比较的时j+1,j+1前一个就是j
}
}
3.处理前后缀相同的情况
如果s[i]与s[j+1]相同,直接后移。并更新next数组。
if(s[i] == s[j+1])
{
j++;
}
next[i] = j; //j同时也表示i之前(包括i)最长相同前后缀的长度。
j为什么还可以表示最大相同前后缀的长度呢?
j初始化为-1,在对i=1与j+1比较之后j++为0,正好是一个字符的最大相同前后缀的长度,后面同理。
最后构建next数组整体代码如下
void getNext(int* next,string s)
{
int j = -1;
next[0] = j;
for(int i = 1; i<len;i++) //len为长度(此处未定义)
{
while(j >= 0 && s[i] != s[j+1])
{
j = next[j];
}
if(s[i] == s[j+1])
j++;
next[i] = j;
}
}
利用next数组进行匹配
在文本串s中寻找模式串t。首先定义两个下标i,j。(i指向模式串起始位置,j依然初始化为-1)为什么j初始化为-1?next记录的起始位置为-1。i从零开始,遍历文本串。
for(int i = 0;i<len;i++)
接下来比较s[i]与s[j+1],(大致与构建next数组相同,只是构建next数组时在一个字符串,而匹配是在两个字符串之间)
while(j>=0 && s[i] != s[j+1])
{
j = next[j];
}
if(s[i] == s[j+1])
j++;
如果j与len-1相等时,说明匹配成功返回文本串中出现模式串的第一个位置。即当前位置i-模式串长度+1;
if(j == len-1)
{
return i-len+1;
}
整体代码如下:
int j = -1;
for(int i = 0;i<len;i++)
{
while(j>=0&&s[i]!=s[j+1])
{
j=next[j];
}
if(s[i]==s[j+1])
j++;
if(j == len-1)
return i-len+1;
}
PS
next的数组还可以改进,称为nextval数组,感兴趣可以自己了解。