KMP算法
今天,寒假第5天。
从昨天开始,就有些体会到内心的疲乏和懈怠了。因为重复而单调的生活很难让内心感到“爽快”,甚至在当今社会显得有些不合时宜。但这种生活也自有它的调味品,那些被人称为“有些挑战性”的算法,就能带来些欢欣、掀起些波澜。今天写的KMP算法就算是其中的一个。
上学期《数据结构》老师说到KMP时就一带而过,因为书上打了星号(还是双星呢**),当时也就没仔细看,但是KMP却在我心中树立起了“伟岸”的形象。今天终于一睹其尊容,不亦快哉?
要讲清楚KMP算法确非易事,这倒不是应为它有多复杂,其实从直观上看,KMP是相当简单的。但是要用准确的语言表述,就感觉有些抽象。
我们看一个书上的例子:
(图1)
上面的串叫做“主串”,下面的串是用来和“主串”匹配的,叫做“模式串”。在比较过程中,在i=3,j=3时不匹配了。怎么办?一般的想法是:让i=2,j=1,再进行比较。
(图2)
但KMP算法告诉我们,不用这样。在KMP算法中i是不会走回头路的。我们看看它是怎么做的。
(图3)
KMP算法直接让模式串向右移动2位。我们此时可能并不理解为什么可以这样。我们不妨再看看KMP算法下一步怎么做。
(图4)
在第二趟的比较过程中,很“不幸”的,模式串的前四个都匹配了,但是第五个和主串出现了不同。那下一步怎么做呢?我们直观观察发现,我们把模式串向右移3位就可以了。
(图5)
我们的直观告诉我们,连图5中的j=1的a也不用比较了,直接从j=2开始就可以了(不知道你的直观是不是这样~如果不是,尝试着多看几遍图4)。为什么中间的步骤都可以省略呢?其实在运行到图4状态的时候,我们已经对主串有所了解了,那就是主串[3…6]和模式串[1…4]一定是一样的。其实,KMP算法的优势就在于利用了我们在匹配过程中,对主串形成的了解,这种了解就是模式串和主串有相同的地方。比如在运行到图4状态时,想要了解主串i=7之前的具体情况,模式串就可以回答我们。
在运行到图4之后,我们接下来该做什么呢?因为主串和模式串已经出现了不匹配,那我们就应该把模式串进行移动。常规的思路就是:把模式串向右移动1位,看看和主串是否匹配;若不匹配,就移动2位。。。直到匹配或是比较到最后。由于我们之前讲过,想要了解主串i=7之前的具体情况,模式串就可以回答我们。我们之所以能直接在图5中直接比较j=2,是因为和相同是相同的(见图6划线部分)。
(图6)
和是一定相同的,因为主串和模式串的这一位已经匹配,要想和相同,只要和相同即可。而这是模式串自存在起就已经决定的。我们是不是可以这样归纳(k的求法):对于某一个j,只需在模式串[1..j-1]中找到两个相同的子串: 模[1..k-1]和 [j-k+1..j-1](要保证k取到最大,这样才最有意义;在示例中k=2),然后我们继续比较主串的i和模式串的k就可以了。(仔细体会!)
对于模式串,它的每一位所对应的k都是确定的,示例中j=5时,k=2。我们可以用一个数组next[]来记录,其中next[j]=k。
KMP算法就是说这样,它在遇到不匹配后,不移动主串的i,而是将主串的第i位和模式串第k位继续比较。k则是在模式串确定后,计算好并保存在next[]数组中的。
//到此处,你应该明白了KMP的基本原理,以及k怎么求。
对于刚才所说的内容,下面有一个数学证明:(基本抄课本上的)
定理:设主串为’s1s2…sn’,模式串为’p1p2。。。pn’。
如果主串中第i个字符和模式串中第j个字符失配,此时主串第i个字符应该与模式串第k(k<j)个字符继续比较,则模式串前k-1个字符,且不存在k’>k满足下列关系式:
‘p1p2。。。pk-1’ = ‘si-k+1si-k+2…si-1’
已经得到的匹配部分是:
‘pj-k+1pj-k+2。。。pj-1’ = ‘si-k+1si-k+2…si-1’
联立上面两式,得到
‘p1p2。。。pk-1’ = ‘pj-k+1pj-k+2。。。pj-1’
(就是说,在模式串中找到的k就是主串第i个字符应该与模式串继续比较的k。)
//证毕
下面说说怎么求next数组。下面是定义。
还是先看看课本上的例子。
(图7)
j==1时next[1]=0;j!=1时,手工算就是看看[1..j-1]最长的相同子串长度([1..k-1]和[j-k+2..j-1])。比如j=4时,前面最长的为[1]和[3],都是’a’,所以k=2。又如j=6时,最长的子串为[1..2]和[4..5],都是’ab’,所以k=3。
那怎么编程呢?如果对每个j都要这样一个个地算,是很费时间的。
我们发现,如果对于next[j]=k,那么next[j+1]则可以根据已经算出的值加以推算。
如果next[j]=k,则有:
‘p1p2。。。pk-1’ = ‘pj-k+1pj-k+2。。。pj-1’
(1) 若pk=pj,则有
‘p1p2。。。pk’ = ‘pj-k+1pj-k+2。。。pj’
也就是说,next[j+1]=k+1.
(2) 若pk!=pj,则有
‘p1p2。。。pk’ != ‘pj-k+1pj-k+2。。。pj’
我们把这个模式串既看成主串,也看成模式串。相当于在比较到主串的j和模式串的k时,发现二者不匹配。根据前面说的,KMP算法会将主串的j和模式串的next[k]继续比较。
(图8)
不妨令k’=next[k].
假如p[j]==p[k’],那就是说:p[j]之前有:
‘p1p2。。。pk’’ != ‘pj-k’+1pj-k’+2。。。pj’
(仔细体会)。
所以有:
next[j+1]=next[k]+1
假如p[j]!=p[k’],那我们依次类推,令k’=next[k’], 看有没有[j]==p[k’]成立.若不成立,重复这个操作…如果找不到这样的k’(1< k’<j),那么next[j+1]=1.
于是有下面的代码:
int GetNext(char *s,int next[])
{
int i = 1,j = 0;
next[1]=0;
while(i<strlen(s)) {
if(j==0 || s[i]==s[j]) {
++i;++j;
next[i] = j;
}
else j = next[j];
}
}
课本上最后对这个代码做了改进。
(图9)
如果next[j]=k,而模式串中p[k]==p[j],那么如果主串中si!=pj,就无需再比较si和pk,而直接可以比较si和pnext[k].
改进后的代码如下:
void GetNext(char t[],int nextval[])
{
int i = 1,j = 0;
nextval[1] = 0;
while(i<t[0]) {
if(j == 0 || t[i]==t[j]) {
++i,++j;
if(t[i]!=t[j]) nextval[i] = j;
else nextval[i] = nextval[j];
}
else j = nextval[j];
}
}
再回过头来,看KMP的主代码,相信已经可以看懂了。
int Index_KMP(char s[],char t[],int pos,int nextval[])
//字符数组第0位不存放数据
{
int i = pos, j = 1;
while(i<strlen(s)&&j<strlen(t)) {
if(!j || s[i]==t[j]) {
++i;++j;
}
else
j = nextval[j];
}
if(j>=strlen(t)) return i-strlen(t)+1;
else return 0;
}
已经写得很多了:)
若有疏漏、错误,欢迎批评指正,多谢!