KMP是一种字符串匹配的算法,本来想给题目加个“浅谈”谦虚一下,大神们都喜欢这么做的。但浅谈什么什么的最讨厌了,一般情况下都很难理解。我的估计更 难理解,所以就不加浅谈这两个字了,下面我们来深入探究一下这个问题。之所以叫它KMP,是因为这个东西是K/M/P三个没事研究研究小白鼠的老学究搞出 来的,各个人的名字的首字母凑一块,就有了这个名字,很难想象如果很多很多人都参与了这个算法的提出的话,这个东西会不会就叫做ABCD..XYZ算法。 闲话少说,切入正题:
串的模式匹配是个很繁琐,很没素质但用的很多的问题,如果你只想用而不想深入研究的话,那就给你一个函数—strstr(),灰常简单实用,效率上也比普 通的kmp要高些。至于怎么用,google一下,你就知道。如果你还想了解一下,那就接着往下听我给你慢慢道来~~首先,上帝会给你两个串,一个叫s, 一个叫t,问你s中包不包含t?我们把t叫做模式串。s叫原串。这是个我们经常碰到的问题,比如你想知道这学年的奖学金有没有你的份,你就要从名单里面找 到你的名字,这就是一个很现实的模式匹配,如果把这个问题拿给小学生,他们很快就会判断出结果,怎么做呢?小学生思维是这样的:以s的每一个字符为起点往 后找,找到和t一样的一个子串就返回yes,否则继续往下找,一直到最后。这样就可以判断s中是不是包含t了。但是,作为一个21世纪的新青年,在党的光 辉下成长,受到共产主义的雨泽并且掌握了计算机技术的大学生,这样做实在是白痴了点。虽然我大学以前都很白痴。我们暂且把这种显而易见的算法,或者就是一 种方法,叫做小学生方法。上升到理论高度,小学生方法的复杂度是多少呢?假设s长n,t长为m,那么最坏情况下复杂度为O(mn)。通过计算机的学习我们 知道,生活往往不会向着最好的那一面发展,我们要把最坏的情况考虑到才能从容不迫。最坏情况就是这种s=aaaaaaaaaaaaaaaab,t = ab,如果按部就班的照小学生方法做,是不是很蛋疼呢?如果我这个东西再长点呢?我了个去,谁爱做谁做去,老子没那个时间。
下面分析一组实例:
S = abcabababcc T = ababc N ,M为S和T的长度,这里N=11 M= 5; 我们用i和j两个数表示指向S和T位置的指针, 那么匹配的意思就是S[i-j+1...i]和T[1...j]完全相同,而当j = M的时候存在这样一个i满足等式,我们就可以说T是S的一个子串。小学生会从S的某一个位置发,和T进行比对,比如初始i = 1,j = 1,如果发现两个字符相同,则i++,j++;当S[i]和T[j]不同的时候(如i = 3,j = 3),则j =1;i = 2,然后继续,即将出发点的位置加一重新和j匹配。而KMP则不然,这种算法是尽可能的利用已经有的信息并且加以利用从而达到避免重复判断降低复杂度的目
的(这个理念很重要,在算法的各个方面均有涉及),当遇到不匹配的字符时,我们暂且称之为“阻塞”,KMP的理念是将T串向右滑动一段距离使 (j->j')从而使得T中j'前的串和S匹配。当然,向右滑动的距离越大越接近真理。由此我们可以知道,对于T串,如果这样滑动了的话,那么必然 T的前j’个字符组成的子串必然和T的后j'个字符组成的串完全相同。这样就达到了我们要的效果。
仔细想想,这个过程就是利用了刚才已经比对的信息。
比如: 1 2 3 4 5 6 7 8 9 10 11
S = a b c a b a b a b c c
T = a b a b c
1 2 3 4 5
为了方便说明,我们省略了前面的比对过程,此时,可以看到,i = 7 j = 4的时候造成i+1,j+1阻塞,小学生的做法是i变为5,j变为1继续,而大学生算法(KMP)就让i不变,j变为2,即变为如下状态,然后继续匹配:
1 2 3 4 5 6 7 8 9 10 11
S = a b c a b a b a b c c
T = a b a b c 此时j以前的字符组成的串和S匹配
1 2 3 4 5
到了这里,要有人怀疑这种算法的正确性了,这样做对吗?答案是毋庸置疑的,如果不对,谁闲着没事来说一个错误的算法,请不要质疑权威!尽管这思想很迂腐。
这个算法的复杂度是多少呢??或许有人说这个东西在最坏情况下的复杂度为也是O(nm).关于复杂度,只有那种一眼能看出来的我才能一眼看出来,这个看了好几眼,实在是不好理解。下面引用Matrix67关于KMP算法复杂度的说明:
为什么这个程序是O(n)的?其实,主要的争议在于,while循环使得执行次数出现了不确定因素。我们将用到时间复杂度的摊还分析中的主要策略,简单地 说就是通过观察某一个变量或函数值的变化来对零散的、杂乱的、不规则的执行次数进行累计。KMP的时间复杂度分析可谓摊还分析的典型。我们从上述程序的j 值入手。每一次执行while循环都会使j减小(但不能减成负的),而另外的改变j值的地方只有第五行。每次执行了这一行,j都只能加1;因此,整个过程 中j最多加了n个1。于是,j最多只有n次减小的机会(j值减小的次数当然不能超过n,因为j永远是非负整数)。这告诉我们,while循环总共最多执行 了n次。按照摊还分析的说法,平摊到每次for循环中后,一次for循环的复杂度为O(1)。整个过程显然是O(n)的。这样的分析对于后面P数组预处理 的过程同样有效,同样可以得到预处理过程的复杂度为O(m)。
这段描述里面提到一个数组P,什么是P数组呢,它又是干什么的呢?其实这个数组就解决了遇到“阻塞”的时候T到底向右滑动多少,滑动到哪里的问题,比如上 面的例子中,i = 7 j = 4时候阻塞,然后j就变为P[j](j= P[j]),i加上1然后继续比对。这就是P数组,也有很多文章里面叫做next数组,一个意思,名字只是一个代号而已。可以看到,P数组是和S没有半点 关系的,完全由T决定,所以我们完全可以预处理出P数组。
关于求P数组,我们放到下面再讨论。先说明一下这个问题:已知P数组,怎么解决问题呢?这其实是一个循环的过程(i每次+1),每次循环开始的时候,如果 S[i]<>T[j+1]的时候,就改变j的值,即令j = P[j],如果还不能满足S[i] = T[j+1],那么继续令j = P[j]直到S[i] = T[j+1]为止。然后j加1 i加1继续循环。关于这个的代码有好几个版本,大体上都差不多,还是Matrix67的最为经典:
01j:=0;02fori:=1
to n do03begin04 while(j>0)
and (B[j+1]<>A[i])doj:=P[j];05 ifB[j+1]=A[i]
then j:=j+1;06 ifj=m
then07 begin08 writeln('Pattern
occurs with shift ',i-m);09 j:=P[j];10 end;11end;
看到这个代码是不是很失望?那么短,草!丫写了那么长的解释。其实这个还可以更短,如果你只要判断是不是匹配的话到第6行就可以直接return true了。下面那些是为了找到所有的匹配位置(即与T匹配的子串的第一个字符的位置);代码的好坏绝对和长短没关系,这是作为一个程序员必须要深刻认识 的。
然后就剩了一个求P数组的问题了,引用上面的话:当j变为j'时,必然T的前j’个字符组成的子串必然和T的后j'个字符组成的串完全相同,当然,这个 j'越大越好~~说明白点,P[j]记录的就是令T串中T[1..j]前k个和后k个相等的一个最大的k值,比如说abaccaaba,P[1] = 0,P[2] = 0,P[3] = 1,P[4] = 0....P[3] =1是因为aba中前1个和后1个组成的子串相同(都是'a')。关于P数组的求法,引用Matrix67对这部分的说明:
预处理不需要按照P的定义写成O(m^2)甚至O(m^3) 的。我们可以通过P[1],P[2],...,P[j-1]的值来获得P[j]的值。对于刚才的B="ababacb",假如我们已经求出了 P[1],P[2],P[3]和P[4],看看我们应该怎么求出P[5]和P[6]。P[4]=2,那么P [5]显然等于P[4]+1,因为由P[4]可以知道,B[1,2]已经和B[3,4]相等了,现在又有B[3]=B[5],所以P[5]可以由P[4] 后面加一个字符得到。P[6]也等于P[5]+1吗?显然不是,因为B[ P[5]+1 ]<>B[6]。那么,我们要考虑“退一步”了。我们考虑P[6]是否有可能由P[5]的情况所包含的子串得到,即是否P[6]=P[ P[5] ]+1。这里想不通的话可以仔细看一下:
1 2 3 4 5 6 7
B = a b a b a c b
P = 0 0 1 2 3 ?
P[5]=3是因为B[1..3]和B[3..5]都是"aba";而P[3]=1则告诉我们,B[1]、B[3]和B[5]都是"a"。既然P[6]不 能由P[5]得到,或许可以由P[3]得到(如果B[2]恰好和B[6]相等的话,P[6]就等于P[3]+1了)。显然,P[6]也不能通过P[3]得 到,因为B[2]<>B[6]。事实上,这样一直推到P[1]也不行,最后,我们得到,P[6]=0。
怎么这个预处理过程跟前面的KMP主程序这么像呢?其实,KMP的预处理本身就是一个B串“自我匹配”的过程。它的代码和上面的代码神似:
1P[1]:=0;2j:=0;3fori:=2
to m do4begin5 while(j>0)
and (B[j+1]<>B[i])doj:=P[j];6 ifB[j+1]=B[i]
then j:=j+1;7 P[i]:=j;8end; int next[110];
void calnext(char *t)
{
int lent = strlen(t);
next[0] = -1;
int i = 0,ct = next[0];
while(i < lent - 1)
{
if(ct < 0 || t[i] == t[ct])
next[++i] = ++ct;
else ct = next[ct];
}
}
int kmp(char *s,char *t) //返回主串中匹配的位置(第一个),如果不匹//配返回-1;
{
int lens = strlen(s);
int lent = strlen(t);
calnext(t);
int i = 0,j = 0;
while(i < lens && j < lent)
{
if(j < 0 || s[i] == t[j])
{
i++;j++;
}
else
j = next[j];
if(j == lent)return (i - j);
}
return -1;
}
本文详细介绍了KMP算法的原理、复杂度分析、P数组的求法以及实际应用。通过实例分析,展示了如何利用KMP算法解决串的模式匹配问题,并探讨了算法的优化策略,帮助读者深入理解并掌握KMP算法。
602

被折叠的 条评论
为什么被折叠?



