KMP算法的原理
这个算法理解起来比较复杂,看了网上很多帖子,写的都很乱,不容易理解。现在结合看过的一些书和视频写一些好理解的笔记,希望能给大家带来帮助:
总的思想还是想要回退的时候能尽量偷懒,利用已知的信息,阮老师讲的很清楚:http://www.ruanyifeng.com/blog/2013/05/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm.html
也就是说在每次不匹配发生回退的时候,尽量能让之前比较过的一致的字符能够不用再重复匹配。
KMP算法的实质是,有时候,字符串头部和尾部会有重复。比如,"ABCDAB"之中有两个"AB",那么它的"部分匹配值"就是2("AB"的长度)。搜索词移动的时候,第一个"AB"向后移动4位(字符串长度-部分匹配值),就可以来到第二个"AB"的位置。
这就引出了最长的真前缀和 真后缀匹配长度的问题,而基于模式串自身的自匹配性,这个长度在给出模式串的时候已经能算出来。
我们把模式串首先遍历一遍,将每个位置之前字串的最大真前缀和真后缀长度事先存在next数组中,这样每当发生一次失配时,在这个数组中查找这个最长的匹配长度,然后移动到这个最长匹配位置。举个例子:
当上面的情况发生空格与D不匹配时,前面六个字符"ABCDAB"是匹配的。查表可知,最后一个匹配字符B对应next数组中的"部分匹配值"为2,即“AB”,因此按照下面的公式算出向后移动的位数:
移动位数 = 已匹配的字符数 - 对应的部分匹配值
因为 6 - 2 等于4,所以将搜索词向后移动4位。
怎么求next数组?
先来考察这样的一个场景,在字符串P[i]和模式串P[j]处发生了失配,KMP会去查表,取出next[j],即用P[t]去取代P[j],让P[t]继续与之前的P[i]相对齐。那么为什么会选定这样的t呢?或者说这样的t又具备哪些必要条件呢?
经过分析我们不难发现,选择这个t的必要条件是:在P[j]之前已经适配的子串中,必须有一个长度为t的前缀和一个长度为t的后缀完全匹配,也就是说这个字串的首部和尾部具有一定的相似性。选择这个t必要条件也就可以表示为:
P[0,t) == P[j-t,j)
将满足下述条件的所有t筛选出来,也就可以得到一个候选集合:
也就是在P[j]的前缀P[0,j)中,所有匹配真前缀和真后缀的长度。
在发生一次失配时,也只有来自这个集合中的t,才有资格来作为下一轮的对齐位置。
而next表,其实就是:
那么我们怎么具体怎么求解next数组呢?这里不妨采用递推策略:
分析不难得出如下结论:
当且仅当P[j]和它的替代者P[ next[j] ]相等时等号成立。
也就是说当模式串的某一字符和它的继任者在这一位置相等时,如上图,那么next[j+1] = next[j] +1;(最长匹配长度变长了一位)
那么如果二者不相等呢?怎么去递推?
我们还是按照原有思路,当一次失配发生,我们调用next数组中的对应值,往最大匹配的位置上滑动。那么如果还是不匹配,我们就在新的模式串位置再次调用next中的值,即next[ next[ j ] ]。这个过程可能持续多步,直到匹配为止。见下图:
因为next[ j ]是表示真前缀和真后缀中的最长匹配长度,故next[j]<j(严格小于) ,故上面这个候选序列只会严格递减,直到下图中的最后面的情况:
在这种情况下,通常会出现问题。因为接下来和P[j]比对的那个字符根本就无从谈起。这时候就是哨兵大显身手的时候了,KMP巧妙的借助了"哨兵"。即让next[0]=-1;在模式串的最前面加上-1的通配符,它和任意字符都可以匹配。因此每当第一个字符不能匹配时,我们就用哨兵来匹配。
分析上述过程,我们发现这其实就是模式串不断自匹配的过程。
代码实现
分析完可以开始写代码了,事实上,求next数组的代码和KMP代码几乎一模一样。差别在于要设置个哨兵,以及只传一个模式串参数。我们只需要记住其中一个就够了。
根据《数据结构(C++版)》KMP算法的伪代码可以用如下伪代码表述:
1. 在串S和串T中分别设置比较的起始下标i和j;
2. 重复下述操作,直到S或T的所有字符均比较完毕;
2.1 如果S[i]等于T[j],继续比较S和T的下一对字符;
2.2 否则将下标j回溯到next[j]的位置,即j = next[j];
2.3 如果j等于-1,则将下标i和j分别加1,准备下一趟比较;
3. 如果T中所有字符均比较完毕,则返回匹配的i-j;
否则返回-1;
至此,我们搞清楚了算法思路,以下是代码:
//求Next数组
void getNext(char * p, int * next)//只需要传入模式串,模式串不断自匹配,既作为母串,又作为匹配串
{
next[0] = -1;//初始化哨兵
int i = 0;
int j = -1;//j代表下方模式串中的最右匹配位置,初始化为哨兵位置
while (i < strlen(p))
{
if (j == -1 || p[i] == p[j])
{
++i;
++j;
next[i] = j;//当前位置匹配,next[j+1]=next[j]+1
}
else
j = next[j];//将当前位置更新为next[j],再次比对
}
}
//KMP算法
int KMP(char * t, char * p)
{
int i = 0;
int j = 0;
while (i < strlen(t) && j < strlen(p))
{
if (j == -1 || t[i] == p[j])
{
i++;
j++;
}
else
j = next[j];
}
if (j == strlen(p))
return i - j;
else
return -1;
}
也可以参考下这篇文章:https://blog.csdn.net/x__1998/article/details/79951598