KMP算法我认为难点在next数组的建立。 看Kmp前我仔细看了下传统的匹配模式,代码为:(首元素存放串的长度)
int Index(SString S,SString T)
{
int i = 1;
int j = 1;
while(i <=s[0] && j<=T[0])
{
if(S[i] == T[j])
{
i++,j++;
}
else
i = i-j+2;
}
if(j > T[0])
return i-T[0];
else
return 0;
}
我认为想深刻理解好KMP算法要和传统匹配匹配结合起来。KMP无非就是利用next数组的信息,即最长前缀子串的长度来令j的值回溯,而不是像传统匹配那样令i的值回溯。
int Index_Kmp(SString S,SString T)
{
int i = 1;
int j = 1;
int next[200];
Get_next(next);
while(i <=s[0] && j<=T[0])
{
if( j==0 || S[i] == T[j] )
{
i++,j++;
next[i] = j;
}
else
j = next[j];
}
if(j > T[0])
return i-T[0];
else
return 0;
}
Kmp最主要的就是next数组的建立,这也是kmp的难点,也是困扰了我很久的地方。
先敲遍next数组的函数以找回感觉:
void Get_next(char *t,int *next)
{
int i = 1,j = 0; //这里我认为是挺重要的,当j为0时,代表i和j都要+1从新的位置重新开始匹配,而next的第一个元素的值是已定的,为0.
next[1] = 0;
while(i <= t[0])
{
if(j == 0 || t[i] == t[j]) /*if语句判断为真的情况有两种:
情况1. 当j==0时,后面的 t[i] == t[j]不用管,因为j==0成立后面的判断就不会执行,也避免了越界问题,
j==0相当于在表明在当前i不变的话,下标j已经回溯到尽头了,不可能再有下标为 j 的字符与下标为i的字符相等。
j不能再回溯了,而想继续匹配的方法就只有令i指向一个新的字符, j重新指向串的首元素,重新再开始匹配。而
表达式i++则令i按顺序指向了一个新的字符,j++后j的值1就相当于重头开始匹配
情况2:当j!=0但t[i] == t[j]时,这种情况下说明有两个字符匹配相等,先假想是第1个(假设j=1)与第i个字符匹配了,那么就表明了
此时的最长前缀肯定不为0。而令最长前缀不为0的策略则就是先让j++,若j原本等于0,则最长前缀就变为1。i++的
意义是使i往前一位,因为第i个字符下的最长相等前缀的长度是储存在next数组的[i+1]里的,所以令i向前一位,然后
令next[i] = j;即最长相等子前缀的赋值
*/
{
i++,j++;
next[j] = i;
}
else //当 j != 0 && t[i] != t[j] 时,既然当前字符不相等了,那就令j回溯到可能相等的位置再匹配,记住,是可能相等。若再不相等则再回溯,直至j回溯到尽头
j = next[j]; //此步是令j回溯,可以将next数组的建立看为从第i个字符开始的串t,与从第j个字符开始的串t 匹配。
}
if(j > T[0])
return i - T[0];
else
return 0;
}
其实还有一点就是因为这里运用的串的数据结构T[0]或S[0]的存放的是串的长度,且串的起始下标是从i开始的,而平时用字符串的话我用的都是从0开始的,且长度是直接用字符串函数strlen()计算出来。若用数据结构,则上述的代码就有了一点改变,但总体结构还是一样的,我是比较完两者的差别,然后才学会了KMP的。
下面贴出普通类型字符串的KMP算法(红色字体为有一些值得注意的细节改变):
int Index_kmp(char *s,char *t) //字符串s和t的下标都是从0开始
{
int i,j,len1,len2,next[200];
len1 = strlen(s);
len2 = strlen(t);
i = j = 0;
Get_next(next);
while(i<len1 && i<len2)
{
if(j == -1 || s[i] == t[j]) //具体先看完该版本的Get_next函数,那个懂了,这里也懂了
{
i++;
j++;
}
else
j = next[j];
}
if(j == len2)
return i-j;
else
return -1; //下标从0开始,所以不能return 0
}
void Get_next(char *t,int *next)
{
int i,j,len;
len = strlen(t);
i = 0; /*这里的i和j的值分别设为0和-1. 这里串的下标是从0开始的,所以必须有个不在next数组下标范围内的值来标识j的值已回溯到尽头,这就是j=-1的第一个功能
第二,i的初始值要比j大1,试想第一次判断语句肯定是要成立的,因为next[1](next的第二个元素)的值其实已经确定了,只不过我们将之放入了循环进行统一设置而已,从第三个元素开始才会出现两种或以上的情况。而第一次执行了if内的语句,i++后的值i一定要等于1,因为下标1代表next数组的第二个元素,而j设置一个刚好小于最小下标的标志值用于判断是否已回溯到尽头。那么第一次的执行结果肯定是i=1,j=0,next[i] = j了,即使next[1] = 0.
这里好像出现问题了,因为按照之前的那个程序next[1]的值是要等于1,next[0]=0的,可是没关系,这样做刚好和字符串的起始下标的改变相适应了。而在前面的Index_kmp函数的 j=-1; 也是因为这原因。
*/
j = -1;
next[0] = -1; //此时的next数组下标从0开始,所以第一个元素的值不能赋为0了,而是刚好比0小1的元素,这么做之后,除了首元素,next的其他位置的
//值都为当前位置下最长前缀的长度,而不是之前的最长前缀+1.
while(i < len)
{
if(j==-1|| t[i]==t[j])
{
i++;
j++;
next[i] = j;
}
else
j = next[j];
}
}
写一个例子来提醒记忆:子串为ababaaaba n=9.
若串的下标是从1开始的,那么next数组的值分别为 next[1...n] = 0,1,1,2,3,4,2,2,3
若串的下标是从0开始的,那么next数则的值分别为 next[0...n-1] = -1,0,0,1,2,3,1,1,2
以上是我自学KMP时的一些心得体会和方法,当时KMP算法困了我很久,而且一旦长时间不用就容易忘记,所以写篇博客来加深印象,也有利于以后的复习。
我知道自己有些地方写的不是那么正确,希望看见的朋友能够指出并共同探讨,我也只是一个刚学数据结构的菜鸟。这也是我csdn上的处女博客,若真有看不过眼的地方请不要喷我,很打击自信的... 更希望能在csdn上认识更多志同道合的朋友一起学习,探讨问题!