您的位置:首页 > 其它

我所理解的kmp算法

2014-11-24 19:08 253 查看
从现在起,这个博客不再是单纯的为了记录刷题的数量,开始侧重于文章的质量,希望能尽量多的反映我自己思维的结果。

以kmp算法为起点,继续学习其他模式匹配算法。

其实我倒觉得,kmp算法的难度被很多人夸大了,包括我的数据结构老师。个人觉得严蔚敏的c语言版数据结构讲的kmp还是很不错的,就是数组下标从1开始让我在理解的时候老是搞混。

首先看看最原始的模式匹配算法对于以下主串及模式串是怎样运行的:



原始的算法从最开始进行匹配,当发现不能匹配的字符时,就回退主串的指针,指向最开始匹配的字符的下一个字符,同时模式串的指针也要回到最开始,重新开始匹配。

原始方法的低效率,其实就是因为有很多已经获得的信息被浪费掉了,比如上图中在第三个字符位置失配,主串的指针要退回到第二个字符,模式串指针回退到第一个字符再重新开始比较。而实际上,在第一遍比较的时候,已经知道了模式串中的前两个字符和主串中的前两个字符相同,但这些有用而又宝贵的信息被直接丢弃了。因此,kmp算法的首要任务,就是尽可能高效的利用这些被丢弃掉得信息。

这里就不得不佩服发明kmp的三位计算机科学家了,想到了这样好的一个方法。

具体说来就是,当失配发生时,并不移动主串的指针,而是利用之前比较得到的信息,尽量向右移动模式串的指针。在这里先不要思考究竟为什么,先看下面的演示:

主串:abccabsdf

模式:abccabc

在模式串的最后一个位置失配,这时,就是利用第一遍比较获得的信息的时候了,显然,主串中的前面一段abccab和模式串的前面一段abccab是相等的,这是第一遍比较获得的有用信息,这个时候回退主串的指针到1显然是不明智的,因为主串的最后面ab和模式串的前两个字符ab是一样的,可以直接把模式串的指针移到2,和主串中的s比较,这里,就通过利用之前得到的信息避免了还要主串中4个字符(ccab)开始比较。

说的容易,具体怎么做呢,程序怎么才能知道要把模式串指针移到2呢? 仔细想想其实不难发现,对于模式串abccabc前面的abccab,最前面的ab和最后面的ab是相等的,而模式串的abccab又和主串中的abccab是相等的,于是就不用比较模式串的前两个字符而可以直接将模式串的第三个字符与主串失配的那个字符比较。如下图:



注意黄色部分的第一个字符失配,移动模式串指针之后,变成:



也就是说,我们要计算的,就是模式串中,每一个字符之前,最大的,和模式串首相等的子串的长度,然后如果在这个字符失配,就可以直接移到这个长度的下一个位置比较,需要注意的是,红色部分要尽量的长,这样才能保证最有效的利用前面比较的信息。

不过说到这里,我才发现,“前面比较的有效信息”这句话其实是不准确的,因为失配时移到哪个位置只与模式串自身有关,与主串没有直接关联。

于是,kmp算法现在的主要问题,就是求模式串每个字符之前,最长的“首尾相同字串”的长度,令人感到有些不可思议的是,这步操作竟然只需要线性的时间。

其实,这里也没有什么特别神奇的地方,细心的观察你也可以发现,在求模式串的最长“首尾相同字串”时,也是在求一个模式匹配的问题:前面的字串作为模式串,后面的作为主串。而这里,我们不需要再次去求前面子串的最长的“首尾相同字串”了,因为显然从前往后遍历时已经把前面的求过了,这里颇有点动态规划的感觉,虽然原理完全不同。

下面对具体的代码进行分析:

主体函数段:

int find(char s[MAX],char t[MAX])//s是主串,t是模式串,t的各字符最长的“首尾相同字串”已经被求出来并将长度值保存在next数组中。
{
int i,j,len1,len2;
len1=strlen(s),len2=strlen(t);
for(i=0,j=0;i<len1;)
{
if(s[i]==t[j]||j==-1)//相等时继续向后比较,注意j==-1的条件是为了特殊的情况:对于第一个字符,在它前面不可能存在<span style="font-family: Arial, Helvetica, sans-serif;">“首尾相同字串”,也就是如果在模式串的第一个字符就失配时,只能将主串的指针向后移动一位,为了与其他字符相区别,就将其next值直接置为-1,这里非常巧妙,j==-1时直接将模式串和主串的指针加1,即将主串指针后移了一位满足要求,又恰好使模式串指针重新指向第一个字符。</span>
j++,i++;
else
j=next[j];//失配时,模式串指针指向该字符的next值
if(j==len2)
return i-len2;
}
return -1;
}


获取next值的函数:
void getnext(char t[MAX])//这个函数和主查找函数几乎一模一样,简直让人感到不可思议。其实是因为确定next值的过程其实也是一个模式匹配的过程。
{
int i,j,len=strlen(t);
next[0]=-1;//正如上面所说,模式串第一个字符的next值为特殊的-1
for(i=0,j=-1;i<len;)//初始时模式串指针j为-1,因为这时还没有得到任何<span style="font-family: Arial, Helvetica, sans-serif;">“首尾相同字串”</span>
{
if(t[i]==t[j]||j==-1)//和模式匹配主算法几乎一模一样!!
{
i++,j++;
next[i]=j;
}
else
j=next[j];
}
}


还要说的是,对于next函数,还可以继续改进:

void getnext1(char t[MAX])
{
int i,j,len=strlen(t);
next[0]=-1;
for(i=0,j=-1;i<len;)
{
if(t[i]==t[j]||j==-1)
{
i++,j++;
if(t[j]!=t[i])
next[i]=j;
else
next[i]=next[j];<span style="font-family: Arial, Helvetica, sans-serif;">//如果当前待确定next值的字符和正常 next值位置的字符一样,就应该将此字符的实际next值置为和j位置字符next值相同</span>

}
else
j=next[j];
}
}


写到这里,才发现自己的理解并没有自己以为的那么彻底,语言表述也觉得没我预想的那么清晰,只是作为自己思考的纪录罢,希望不会误导初学者。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: