KMP算法
一、引言
主串(被扫描的串):S=‘s0s1...sn-1’,i 为主串下标指针,指示每回合匹配过程中主串的当前被比较字符;
模式串(需要在主串中寻找的串):P=‘p0p1...pm-1’,j 为模式串下标指针,指示每回合匹配过程中模式串的当前被比较字符。
字符串匹配:在主串中扫描与模式串完全相同的部分,并返回其在主串中的位置,这里的起始扫描位置默认为主串的第一个字符,即默认pos=0,其他情况类似。
朴素匹配算法:在模式串与主串的匹配过程中,一共要进行n=Length(S)回合的匹配,每一回合分别从主串的起始字符s0,s1,...,sn-1开始进行。在具体某一回合的匹配过程中,每当模式串P中的某一字符与主串S中的被比较字符不相等,主串S的指针 i 都必须回溯到此回合起始字符的下一个位置,模式串P的指针 j 回到模式串串首,重新进行下一回合匹配。算法最坏情况下的时间复杂度为O(m*n)。这里不再详述。
KMP匹配算法:KMP是一个高效的字符串匹配算法,它是由三位计算机学者 D.E.Knuth 与 V.R.Pratt 和 J.H.Morris 同时发现的,因此人们通常简称它为 KMP 算法。在KMP匹配过程中,每当模式串P中的某一字符与主串S中的被比较字符不相等,主串S的指针 i 不需要回溯,而只要将P串“向右滑动”到一定位置,继续进行下一回合的比较。KMP算法的时间复杂度为O(m+n)。
下面主要理解KMP匹配算法。
1)先由KMP算法的主要思想得到next函数的定义
2)然后根据next函数定义求取next函数值
3)最后根据next函数值进行主串、模式串匹配
附:根据next函数定义一眼看出来next函数值。
二、定义next[j]
我们要解决的关键问题是:当本回合匹配过程中出现失配时,下回合匹配时模式串“向右滑动”的可行距离有多远,也即:本回合匹配过程中,主串S的第 i 个字符与模式串P中的第j个字符失配时,下回合匹配时主串中的第i个字符应与模式串中的哪个字符进行比较(设为next[j],因为是“向右滑动”,故next[j]<j)。
引例:假设有以下主串和模式串:
i: | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
S: | a | c | a | b | a | a | b | a | a | b | c | a | c | a | a |
P: | a | b | a | a | b | c | a | c | |||||||
j: | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
a)。。。之前匹配步骤
b)在经过若干回合匹配之后,两字符串状态如下(i=1,j=0):
i: | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
S: | a | c | a | b | a | a | b | a | a | b | c | a | c | a | a |
P: | a | b | a | a | b | c | a | c | |||||||
j: | -1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
c)此时主串的第1个字符与模式串的第0个字符不等,主串的指针i不回溯,由于next[j]<j,考虑定义:next[j] =next[0]= -1。模式串指针j向右移动至位置next[0]= -1,情形如下。(位置"j=-1"是假想的,在后面我们将会发现这样处理的好处)
i: | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
S: | a | c | a | b | a | a | b | a | a | b | c | a | c | a | a |
P: | a | b | a | a | b | c | a | c | |||||||
j: | -1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
d)此时,模式串再也无法向右滑动,此轮匹配失败,令i和j均自增,继续进行比较,情形如下(i=2,j=0):
i: | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
S: | a | c | a | b | a | a | b | a | a | b | c | a | c | a | a |
P: | a | b | a | a | b | c | a | c | |||||||
j: | -1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
e)此时字符相等,主串的指针i和模式串的指针j均自增1,继续匹配结果如下(i=3,j=1):
i: | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
S: | a | c | a | b | a | a | b | a | a | b | c | a | c | a | a |
P: | a | b | a | a | b | c | a | c | |||||||
j: | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
f)。。。如此重复e)
g)当i=7,j=5时,主串的与模式串当前字符不相等。情形如下(i=7,j=5):
i: | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
S: | a | c | a | b | a | a | b | a | a | b | c | a | c | a | a |
P: | a | b | a | a | b | c | a | c | |||||||
j: | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
此时,主串的指针i不回溯,那么模式串将向右滑动至什么位置呢?
我们假设next[j]=k(k<j)。
①本回合,si与pj失配(i=7,j=5),而在失配之前有“部分匹配”,故有:'p0p1...pj-1'='si-jsi-j-1...si-1',又因为k<j,从而'p0p1...pj-1'的部分串'pj-kpj-k+1...pj-1'满足:'pj-kpj-k+1...pj-1'='si-ksi-k+1...si-1';
②下回合,因为要保证从模式串的k位置处字符开始比较,那么必须保证模式串k位置之前的部分串'p0p1...pk-1'满足:'p0p1...pk-1'='si-ksi-k+1...si-1',其中k取最大的可能值;
由①式、②式右半边相等可知,k的取值满足下面的判别等式:'pj-kpj-k+1...pj-1'='p0p1...pk-1',其中k取该解集中的尽可能大值。考虑定义:next[j] = Max{k|1<k<j且'pj-kpj-k+1...pj-1'='p0p1...pk-1'}。
上式也即:j位置之前的P尾串 = 0位置(包括0)之后k位置之前的P头串,如上图中:'p3p4'='p0p1',故k=2,它反映了模式串“在j位置之前的P尾串”与“0位置(包括0)之后k位置之前的P头串”的重复程度。
h)模式串向右滑动至位置next[j]=2,情形如下(i=7,j=2):
i: | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
S: | a | c | a | b | a | a | b | a | a | b | c | a | c | a | a |
P: | a | b | a | a | b | c | a | c | |||||||
j: | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
j)。。。后续匹配
根据以上讨论,对k(next[j])考虑如下定义:
(1)next[j] = -1(j=0);
(2)next[j] = Max{k|1<k<j且'pj-kpj-k+1...pj-1'='p0p1...pk-1'}(j!=0且集合有解);
(3)next[j] = 0(j!=0且集合无解);
三、求取next[j]
根据next[j]的定义可知,此函数的值取决于模式串的本身以及模式串的失配位置。此时把求解next[j]问题看成是一个模式串自匹配问题,即主串和模式串都是P,从主串P的p1开始与模式串P的p0开始进行匹配:
当j=0时,由定义得:next[0]=-1;
当j!=0时,我们思路是:按照主串的下标j由小及大,依次求next[1]、next[2]、...、next[m-1],m为模式串的维数。现在假设已知next[j]=k,且next[t](t<j)均已求得。如果求得next[j+1]与next[j]的关系,那么所有的next函数值均可被求出。
此时,由next[j]的定义可知:'pj-kpj-k+1...pj-1'='p0p1...pk-1',且k为最大值,下面分两种情况讨论:
(a)如果pj=pk,结合'pj-kpj-k+1...pj-1'='p0p1...pk-1'可以得到'pj-kpj-k+1...pj-1pj'='p0p1...pk-1pk',又不存在k'>k满足该式,由next函数的定义可知:next[j+1]=k+1,也即:next[j+1]=next[j]+1。这个式子的意味着,该情况下主串字符指针(j+1)位置处的next[j+1]可以由当前j位置处的next[j]加1求得。(由于下标最小的next函数值next[1]=0是已知的,这使得按下标由小及大的顺序求解所有next函数值成为可能,这种情况对应着下面伪代码的 if(P[i] == P[j])语句部分)
(b)如果pj!=pk,将模式串向右滑动至k'(k'=next[k]<k<j)位置,使得主串的pj字符与模式串的pk'字符比较。
①如果此时pj=pk'(k'<k),结合'pj-kpj-k+1...pj-1'='p0p1...pk-1',则有'pj-k'pj-k'+1...pj-1pj'='p0p1...pk'-1pk'',由next函数的定义该式等价于:next[j+1]=k'+1=next[k]+1(观察下标k<j,由于next[t](t<=j)均为已知,则一定可以求出next[j+1])。
②如果此时pj!=pk',则将模式串继续向右滑动,直至pj和模式串的某个字符pk_lucky匹配成功,此时pj=pk_lucky(k_lucky<k),结合'pj-kpj-k+1...pj-1'='p0p1...pk-1',则有'pj-k_luckypj-k_lucky+1...pj'='p0p1...pk_lucky',由next函数的定义该式等价于:next[j+1]=k_lucky+1=next[...next[k]...]+1(在几次连续的滑动过程中,每次迭代k'=next[k],k'<k<j恒成立,由于next[t](t<=j)可知已知,则一定可以求出next[j+1])。
①和②的讨论说明,无论经过多少次滑动,只要主串的pj最终与模式串pk_lucky字符匹配成功,则主串字符指针(j+1)位置处的next[j+1]一定可以由next[t](其中t<=j)加1求得(这种情况对应着下面伪代码的else语句部分)。
③尽管向右滑动,一直到j=next[t]=-1,很不幸找不到k'使得pj=pk',这相当于匹配过程中无解,此时由定义知next[j+1]=0(这种情况对应着下面伪代码的if(j==-1)部分)。
故伪代码如下:
void get_next(SString P, int next[]){ //求模式串P的next函数值并存入数组next中,i、j分别代表主串、模式串的下标 i = 0; j = -1; next[0] = -1; while(i < len(P)-1){ if( j==-1 || P[i] == P[j] ) { ++i; ++j; next[i] = j; }//每当自增i和j,得到一个新的next[i] else j = next[j];//模式串向右移动 } }
例子:有以下模式串:
P: | a | b | a | a | b | c | a | c |
j: | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
算法的前几次迭代过程列举如下:
a)执行if,++i,++j,进入初始状态(i=1,j=0),next[1]=0:
i: | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | |
P: | a | b | a | a | b | c | a | c | |
P: | a | b | a | a | b | c | a | c | |
j: | -1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
b)执行else,模式串向右滑动(i=1,j=next[0]=-1):
i: | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | ||
P: | a | b | a | a | b | c | a | c | ||
P: | a | b | a | a | b | c | a | c | ||
j: | -1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
c)j==-1,执行if,++i,++j(i=2,j=0),next[2]=0:
i: | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | ||
P: | a | b | a | a | b | c | a | c | ||
P: | a | b | a | a | b | c | a | c | ||
j: | -1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
d)pi=pj,执行if,++i,++j(i=3,j=1),next[3]=1:
i: | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | ||
P: | a | b | a | a | b | c | a | c | ||
P: | a | b | a | a | b | c | a | c | ||
j: | -1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
e)执行else,模式串向右滑动(i=3,j=next[1]=0):
i: | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | |||
P: | a | b | a | a | b | c | a | c | |||
P: | a | b | a | a | b | c | a | c | |||
j: | -1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
以此类推。。。
最后求得next[j]如下:
j: | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
P: | a | b | a | a | b | c | a | c |
next[j] | -1 | 0 | 0 | 1 | 1 | 2 | 0 | 1 |
四、改进的next函数值算法
这样的改进已经是很不错了,但算法还可以改进,注意到下面的匹配情况:
i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
S | a | a | a | b | a | a | a | a | b |
P | a | a | a | a | b | ||||
j | 0 | 1 | 2 | 3 | 4 | ||||
next[j] | -1 | 0 | 1 | 2 | 3 | ||||
nextval[j] | -1 | -1 | -1 | -1 | 3 |
模式串P中的p3='a'和主串S中的s3='b'失配时,P向右滑动至next[3]=2位置,由于p2=p3='a',此时的比较还是会失配;然后P向右滑动至next[2]=1位置,由于p1=p2='a',此时的比较还是会失配;P向右滑动至next[1]=0位置,由于p0=p1='a',此时的比较还是会失配,P向右滑动至next[0]=-1位置。
如果我们能够修正next[1]=next[next[1]]=next[0]=-1,修正next[2]=next[next[2]]=next[1](修正后的值)=-1,修正next[3]=next[next[3]]=next[2](修正后的值)=-1,使得出现类情况的时候,P都能够一次性直接滑动到next[0]=-1的位置,这相当于我们在求next函数值的时候,把这些冗余的比较进行预处理,如此就可以消除模式串与主串之间这样的多余比较。
设主串中的si和pj失配,按原定义模式串中应滑动到位置k=next[j],进行si和pk的匹配,而模式串中pk满足pk=pj,因此si不需要再和pk进行比较,而直接和pnext[k]匹配,也就是说此时的next[j]=next[k]=next[next[j]]。
i | 0 | 1 | 2 | 3 | 4 | |
P | a | a | a | a | b | |
P | a | a | a | a | b | |
j | 0 | 1 | 2 | 3 | 4 | |
next[i] | -1 | 0 | 1 | 2 | 3 | |
nextval[i] | -1 | -1 | -1 | -1 | 3 |
对上面的get_next函数稍加改进得到:
void get_nextval(SString P, int nextval[]){ //求模式串P的next函数修正值并存入数组nextval i = 0; j = -1; nextval[0] = -1; while(i < len(P)-1){ if(j == -1 || P[i] == P[j]){ ++i; ++j; if(P[i] != P[j]) nextval[i] = j;// else nextval[i] = nextval[j];//nextval[i]=j意味着当主串的当前字符与模式串中的pi不匹配时,应与模式串的pj比较。而当pj=pi时,所以应该与模式串的pnext[j]比较。
}
else j=nextval[j];
}
}
五、KMP算法
求得next[j]的函数值之后,我们就可以根据KMP算法进行字符串匹配了。
在匹配过程中,如果si=pj,则i和j分别增1;
否则,i不变,j移动到next[j]的位置继续比较,以此类推,直至下面两种可能:
1)j退至某个next值(next[next[…next[j]...]])时字符比较相等,则指针各自增1,继续进行匹配;
2)退到next[next[…next[j]...]]为-1,此时需要将模式串向右滑动一个位置,即从主串的下一个字符si+1起和模式串p0开始重新匹配。
KMP算法伪代码如下:
int Index_KMP(SString S, SString P, int pos){ //利用模式串P的next函数求P在主串S中第pos个字符之后的位置的KMP算法。 //其中,P非空,0<=pos<StrLength(S)。 i = pos; j = 0; while(i < len(S) && j < len(P)){ if(j == -1 || S[i] == P[j]) { ++i; ++j; }//继续比较后继字符 else j = next[j];//模式串向右滑动 } if(j == len(P)) return i - len(P);//匹配成功 else return 0; }//Index_KMP
六、直接看出next[j]函数值(这部分可以用,不过原理不太清晰)
根据以上next[j]的定义:
(1)next[j] = -1(j=0);
(2)next[j] = Max{k|1<k<j且'pj-kpj-k+1...pj-1'='p0p1...pk-1'}(j!=0且集合有解);
(3)next[j] = 0(j!=0且集合无解);
可以直接看出模式串的next函数值。这主要遵循2条规则:
a)如果上一个next[j]!=-1,next[j+1]=Max{k|1<k<j且'pj-kpj-k+1...pj-1'='p0p1...pk-1'};
b)如果上一个next[j]=-1,由next[j]的求解算法知,此时主串第j位与模式串第0位对齐,比较这两位:①如果不等,则主串的第j+1位与模式串的第0位对齐,故next[j+1]=0;②如果相等,则next[j+1]=Max{k|1<k<j且'pj-kpj-k+1...pj-1'='p0p1...pk-1'}
这篇文章是我理解KMP的思路,欢迎批评指正。
参考文献:《数据结构》,严蔚敏著