百度百科: KMP算法是一种改进的字符串匹配算法,由D.E.Knuth与V.R.Pratt和J.H.Morris同时发现,因此人们称它为克努特–莫里斯–普拉特操作(简称KMP算法)。KMP算法的关键是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是实现一个next()函数,函数本身包含了模式串的局部匹配信息。也叫看毛片算法,总之贼厉害。
近两天在学习这个算法时,开始觉着还好,到了这个next数组时,书上虽然分析了一波next数组的由来,然后直接上代码,但是第一次看属实没看懂,智商是硬伤,然后查阅了相关资料,算是明白了过程。
前面的算法原理以及它相对朴素算法简略的步骤在这里就不说了,比较容易理解,大概意思就是对于子串中首字符与后面的字符均不相等时,或者字串中有与首字符相等的字符就可以省略一部分的判断步骤。
在之前的朴素的模式匹配算法中,主串的i值是不断回溯来完成的,而 KMP算法就是为了让这没必要的回溯不发生,所以要考虑的就是子串中的j值如何变了(j值与主串是没有多大关系的,这点要明白),而j值的变化就定义为一个数组next,其长度就是T串的长度。
我们知道如果主串与子串如果在某个字符不匹配,下一个与主串匹配的字符是谁(即j的值)取决于当前子串不匹配字符的前面字符前后缀的相似度。
T | 9 | a | b | a | b | a | a | a | b | a |
---|---|---|---|---|---|---|---|---|---|---|
下标j | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
next |
比如这里有个模式串T,此处串T当成以数组的形式存储,所以下标j==0时一般储存的是数组长度9。
T | 9 | a | b | a | b | a | a | a | b | a |
---|---|---|---|---|---|---|---|---|---|---|
下标j | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
next | 0 | 1 | 1 | 2 | 3 | 4 | 2 | 2 | 3 |
下面开始分析next数组的值如何得来:
首先定义T【i】为模式串中不匹配字符之前字符的后缀的单个字符,T【j】为前缀。
当T串的第一个字符就与S串的第一个字符不同时,此时T串第一个字符就与S串的第二个个字符对应,也就相当于T串整体往后移一位,而此时对应S串第一个字符正好可以看成是下标j==0时的T值9,所以有next[1] = 0.与表格结合起来意思就是第一个字符a与S串不匹配,下一步就是让代表T中第0个位置(也就是9)的代表a与S串的字符匹配,a与S串的下一个匹配。
当i=1,j=0时,存储的是字符串T中的数组长度为9,没多大意义,执行 ‘++i;++j’ ,然后
if(j==0 || T[i]==T[j]) //T[i]表示后缀的单个字符,T[j]表示前缀的单个字符
{
++i;
++j;
next[i] = next[j];
}
else
j = next[j]; //若字符不相同,j值回溯
按照上面的代码当i=2,j=1时,根据表格T[2]=b≠T[1]=a,所以执行代码中j = next[j] =next[1]=0 (这样写只是为了方便理解),然后回到循环执行++i;++j;后此时i=3,j=1,然后执行next[3]=j=1,继续T[3]=a=T[1],然后++i;++j; 此时i=4,j=2,next[4]=j=2,这里的2的意思是如果在T串中的第i=4个字符b与主串S不匹配时,下一步就是用T串中的第next[4]=2个字符来代替b与S串匹配,然后再继续就可得出表格中的值,此处不一一写出。
此处要注意的是当下标为4时,此时字符b前面的字符为aba,前缀为a,后缀也为a,而next[4]=2; 当下标为5时,此时字符a前面的字符为abab,前缀为ab,后缀也为ab,而next[5]=3; 当下标为6时,此时字符a前面的字符为ababa,前缀为aba,后缀也为aba,而next[6]=4,不难发现next数组的值比前后缀相同个数大一。当T[6] ≠ T[4]时,其我们知道执行的是j值回溯,其实这个回溯的原理和T串匹配S串的原理是差不多的,它相当于T串与自身的匹配,当两者不等时,其回溯的目的就是为了找出字符串前后缀相等的个数,从而得出下一步该用T串哪一个字符(即下标对应next的值)与S中字符去匹配。可以根据表格自行设置S串的字符去验证加深理解。
以下附上完整的代码:
void get_next(String T, int *next)
{
int i, j;
i = 1;
j = 0;
next[1] = 0;
while ( i < T[0] ) //T[0]代表串T的长度
{
if(j==0 || T[i]==T[j])
{
++i;
++j;
next[i] = next[j];
}
else
j = next[j]; //若字符不相同,j值回溯
}
}