前言
文章介绍不同next数组的区别以及相应的KMP算法的区别,并解释一下生成next数组的原理。
#KMP算法是什么:KMP算法是字符串匹配中比BF算法(朴素的搜索算法)更加快速的算法。KMP算法中指向主串的i指针(这里的指针不是指内存地址,是遍历算法中常用的伪指针)永不后退,而子串中的 j 指针在失配时不会每次都退回首元素的位置,而会根据next数组跳过一些没必要匹配的元素。
BF算法
int Index(String S, String T, int pos)
{
int i = pos;
int j = 1;
while(i <= S.Len && j <= T. Len)
{
if(S.ch[i] == T.ch[j]) //对成功匹配的处理
{
i++;
j++;
}
else //对失配的处理
{
i = i-j+2;//i退回上次匹配首位的下一位
j = 1; //j退回子串T的首位
}
}
if(j > T.Len)
return i-T.Len;//匹配成功
else
return 0;
KMP算法(第三种next数组)
int Index_KMP(String S, String T, int pos)
{
int i = pos;
int j = 1;
while(i <= S.Len && j <= T. Len)
{
if(S.ch[i] == T.ch[j]) //对成功匹配的处理
{
i++;
j++;
}
else //对失配的处理
{
j = next[j]; //i不用回溯,j根据next数组回溯
}
}
if(j > T.Len)
return i-T.Len;//匹配成功
else
return 0;
可以看出来,KMP算法只是在失配的时候修改一下操作。使得 i 不再无脑回退,j 也不再无脑回退到子串的首位,而变成根据next数组跳过一些多余的匹配过程。
一、四种next数组的区别
0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
a | a | b | a | a | f |
0 | 1 | 0 | 1 | 2 | 0 |
0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
a | a | b | a | a | f |
-1 | 0 | -1 | 0 | 1 | -1 |
0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
a | a | b | a | a | f |
-1 | 0 | 1 | 0 | 1 | 2 |
1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|
a | a | b | a | a | f |
0 | 1 | 2 | 1 | 2 | 3 |
关于整体右移优化的影响:
前缀表,也就是第一种next数组的使用:
int Index_KMP(String S, String T, int pos, int next[])
{
i = start; //主串
j = 0; //子串
while(i < S.length && j < T.length)
{
if( S.str[i] == T.str[j]) //字符匹配
{
i++;
j++;
}
else if(j > 0)
j = next[j-1]; //根据next数组跳过一些不必要的匹配
else //比较第一个字符时失配(j == 0)
i++;
}
if(j == T.length)
return i-j;
else
return -1;
}
整体右移也就是第三种next数组的使用:
int Index_KMP(String S, String T, int start, int next[])
{
i = pos; //主串
j = 0; //子串
while(i < S.length && j < T.length)
{
if(j == -1 || S.str[i] == T.str[j]) //相等前后缀长度为0或字符匹配
{
i++;
j++;
}
else if(j > -1)//这个if可以去掉
j = next[j]; //根据next数组跳过一些不必要的匹配
//else //比较第一个字符时失配(j == 0)
//{
// i++;
// j++;
//}
}
if(j == T.length)
return i-j;
else
return -1;
}
注意到右移后发生了一个语句变化: j = next[j-1] 变成了 j = next[j]
我们可以把next[ ]看成一个取对应元素前面的最长相等前后缀长度的函数(后面会有图解释)。因为next本身存储的就是最长相等前后缀长度。对元素f,取它最长相等前后缀长度在前缀表中就是next[5-1]也就是2,在右移后就是next[5]也是2。
索引 | 0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|---|
模式串 | a | a | b | a | a | f |
前缀表 | 0 | 1 | 0 | 1 | 2 | 0 |
右移后 | -1 | 0 | 1 | 0 | 1 | 2 |
右移后还可以将比较第一个字符时失配的情况与匹配成功的情况合并 。因为匹配成功后是 i 和 j 都要+1来匹配下一个字符。而第一个字符失配只说明 i 必须+1。如果要合并的话就应该消除 j++;的影响。所以右移后next数组第一位减一。
整体右移且整体加一也就是第四种next数组的使用:
int Index_KMP(String S, String T, int start, int next[])
{
i = start; //主串
j = 1; //子串
while(i < S->length && j < T.length)
{
if( j == 0 || S->str[i] == T.str[j]) //相等前后缀长度为0或字符匹配
{
i++;
j++;
}
else
j = next[j]; //根据next数组跳过一些不必要的匹配
}
if(j > T.length)
return i-T.length+1;
else
return 0;
}
整体加1后,因为是对索引从1开始的字符串的匹配,所以对于主串和子串的匹配都应该从1开始,所以start不能为0 且 j = 1。并且最后返回位置的时候也应该+1。
二、生成next数组算法的难点剖析
int get_next(String T, int next[])
{
/*next数组,next[0] = -1,子串索引从0开始*/
int i = 0;
int j = -1;
next[0] = -1;
while(i < T.length)
{
if (j == -1 || T.str[i] == T.str[j])
{
i++;
j++;
next[i] = j;
}
else
j = next[j];
}
}
这里的 i 和 j 和简单比较用的双指针不一样。i 指向的是后缀的末尾,j指向的是前缀的末尾。同时前缀前面是没有字符的,所以可以将 j 的值理解为最长相等前后缀的长度。
学习KMP最难的就是理解生成next数组时的回退:j= next[j]。在这里我们可以把next[ ]看成一个函数,功能为取某字符前面的最长相等前后缀的长度。这时取得的next[j]作为下标刚好就跳过了前缀。说起来有点复杂,让我们用图来解释:
索引 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
---|---|---|---|---|---|---|---|---|---|
模式串 | a | f | a | c | a | f | a | f | e |
右移后 | -1 | 0 | 0 | 1 | 0 | 1 | 2 | 3 | ? |
好,这时我们需要求next[8]。所以此时 i = 7,由图可知 j = 3。
可知 T.str[i] != T.str[j],即c != f(为什么是 T.str[i] 和 T.str[j] 比较?因为 j 是最长相等前后缀的前缀末。如果相等那么说明最长相等前后缀长度可以+1。如果是 j == -1时呢?这时候其实长度也是0,只是让 j-1 可以抵消 j++;的影响,实际上,j 变成-1就立马 j++;了)如果是等于则最长相等前后缀从abc变成abaf。j = 4;next[8] = j;既然不相等,那么执行语句:j = next[j]后比较。如果还不相等则继续执行语句:j = next[j]后继续比较。其实这就是一种递归:j next[j] next[ next[j] ]
next[j]长这样:
next[ next[j] ]长这样:
所以,next[j]实际上就是取j前面字符的最大相等前后缀的长度