从《严书》上看到了KMP算法,看了一遍没懂,但觉得挺神奇的,就花费了几天时间深入的理解。
算法的原理其实不难,难的就是那个巧妙的next数组,这个next数组很吸引我,我的大部分时间也都是花费在这个数组上面的。这个next数组是KMP里面一个很关键的地方,对于在数据结构书上看过一遍整个算法流程的人,能够把next数组搞明白,整个KMP算法的整体思想就差不多理解了。然后在一些细节上面深入思考一下,就可以理解和领会改进的KMP算法。
一、KMP算法简单介绍
KMP算法是字符串匹配算法的一种,相对于朴素的字符串匹配算法而言,可以大大避免重复遍历的情况。此算法可以在O(n+m)的时间数量级上完成字符串匹配操作。
二、神奇的next数组
关于KMP算法的原理和实现,书上或者百度一下都可以找到,我在这里就不罗嗦那么多了,直接切入主题(next数组)。
我们设主串S=abcabcabca,模式串p=abcabx。
KMP第一趟匹配:
i=6
S : a b c a b c a b c a
位置 : 1 2 3 4 5 6 7 8 9 10
P : a b c a b x
位置 : 1 2 3 4 5 6
j=6
第一次匹配到第6个位置的时候失败了,按照朴素的算法,i要回溯到第2个位置,j要回溯到第1个位置重新匹配。KMP的话,主串中的i是不会回溯,模式串中的j回溯也不会回溯到第1个位置。注意这里是关键,i不用回溯就可以完成整个字符串的匹配。为什么i不需要回溯呢?我们先留下这个疑问。
我们把匹配成功的前5个字符研究一下。
1位置的前缀子串为:a , ab , abc , abca
5位置的后缀子串为:bcab , cab , ab , b
我们观察发现两组里面都有一个ab,你能看出点什么东西么,好的,先不管这个。
我们就按照朴素的算法来看,i回溯到第2第3位置都会在前5个字符中匹配失败。
朴素匹配:
i=4
S : a b c a b c a b c a
位置 : 1 2 3 4 5 6 7 8 9 10
P : a b c a b x
位置 : 1 2 3 4 5 6
j=1
当回溯到第4个位置的时候,成功匹配的字符为ab,然后再去判断S串的第6个字符和P串的第3个位置。这个然后我们先不管,观察S中和P匹配的ab,在第一趟匹配的时候S中的ab是和P中前5个字符的最后两个匹配的,而这一次匹配则是和P中前两个字符匹配的。能发现点什么东西么?
不需要让i回溯到之前的位置重新匹配,只需要找到在P串前5个字符中第一个位置的前缀子串和最后一个位置的后缀子串相等并且串长最大的那一对子串,让j指向前缀子串最后一个字符的下一个位置3,和i所指向的6进行比较。往后遇见不匹配的时候采取和这个一样的方法。
KMP第二趟匹配:
i=6
S : a b c a b c a b c a
位置 : 1 2 3 4 5 6 7 8 9 10
P : a b c a b x
位置 : 1 2 3 4 5 6
j=3
这个时候就需要next数组的建立了,next[6]存储的就是前5个字符组成的字符串中的第一个位置的前缀子串和最后一个位置的后缀子串相等并且串长最大的那一对子串的最后一个字符的下一个位置,也就是3,也就是和P串中第3个位置匹配。
写到这里,next数组应该可以得出来了。
具体代码怎么得出来的,书上面都有。。那个应该不难。
对于next数组还有一个优化,《严书》上讲的很清晰。
三、next数组在ACM中的应用
直接用KMP算法真的去匹配两个字符串其实很少见,除非字符串里的字符集范围很小,或字符重复数量过多,用KMP可大减少时间,否则一般都是直接朴素匹配。
kmp算法在ACM中并不大可能用来直接用,主要有用的是对它的理解和它的精华部分----求 next数组,这个的一个用途就是确定重复子串,具体参见 poj2406,poj1961,poj2752。
next数组模板
void get_next(string s,int next[])
{
int length=s.length();
int i=0,j=-1;
next[0]=-1;
while(i<length)
{
if(j==-1||s[i]==s[j]) /*s[i]表示后缀的单个字符*/
/*s[j]表示前缀的单个字符*/
{
++i;
++j;
next[i]=j;
}
else
j=next[j]; /*若j值不相同,则j值回溯*/
}