文中的公式图片在chrome浏览器下显示不正常,IE则显示正常。
一、引言
主串(被扫描的串):S=,i 为主串下标指针,指示每回合匹配过程中主串的当前被比较字符;
模式串(需要在主串中寻找的串):P=,j 为模式串下标指针,指示每回合匹配过程中模式串的当前被比较字符。
字符串匹配:在主串中扫描与模式串完全相同的部分,并返回其在主串中的位置,这里的起始扫描位置默认为主串的第一个字符,即默认pos=1,其他情况类似。
朴素匹配算法:在模式串与主串的匹配过程中,一共要进行n=Length(S)回合的匹配,每一回合分别从主串的起始字符、、...、开始进行。在具体某一回合的匹配过程中,每当模式串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函数值进行主串、模式串匹配
4)文章最后,根据next函数定义一眼看出来next函数值(有待考证)。
二、定义next[j]
我们要解决的关键问题是:当本回合匹配过程中出现失配时,下回合匹配时模式串“向右滑动”的可行距离有多远,也即:本回合匹配过程中,主串S的第 i 个字符与模式串P中的第j个字符失配时,下回合匹配时主串中的第i个字符应与模式串中的哪个字符进行比较(设为next[j],因为是“向右滑动”,故next[j]<j)。
假设有以下主串和模式串:
i: |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
11 |
12 |
13 |
14 |
15 |
16 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
a)。。。之前匹配步骤
b)在经过若干回合匹配之后,两字符串状态如下:
i: |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
11 |
12 |
13 |
14 |
15 |
16 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
c)此时主串的第i(i=2)个字符与模式串的第1个字符不等,主串的指针i不回溯,从而保持不变,由于next[j]<j,模式串指针j向右移动至位置next[1]= 0,情形如下。考虑定义:next[j] =next[1]= 0。(位置"j=0"是假想的,在后面我们将会发现这样处理的好处)
i: |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
11 |
12 |
13 |
14 |
15 |
16 |
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 |
8 |
d)令i和j均自增,继续进行比较,情形如下:
i: |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
11 |
12 |
13 |
14 |
15 |
16 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
e)此时字符相等,主串的指针i和模式串的指针j均自增1,继续匹配结果如下:
i: |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
11 |
12 |
13 |
14 |
15 |
16 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
f)。。。如此重复e)
g)当i=8,j=6时,主串的与模式串当前字符不相等。情形如下:
i: |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
11 |
12 |
13 |
14 |
15 |
16 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
此时,主串的指针i不回溯,那么模式串将向右滑动至什么位置呢?
我们假设next[j]=k(k<j)。
①本回合,与失配(i=8,j=6),而在失配之前有“部分匹配”,故有:,又因为k<j,从而的部分串满足:;
②下回合,因为要保证从模式串的k位置处字符开始比较而不回溯,那么必须保证模式串k位置之前的部分串满足:,其中k取最大的可能值;
由①式、②式右半边相等可知,k的取值满足下面的判别等式:,其中k取该解集中的尽可能大值。考虑定义:next[j] = Max{k|1<k<j且}。
上式也即:j位置之前的P尾串 = 1位置之后的P头串,如图中:,k=3,反映了模式串在j位置之前的P尾串 与1位置之后的 P头串的重复程度。
f)模式串向右滑动至位置next[j]=3,情形如下:
i: |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
11 |
12 |
13 |
14 |
15 |
16 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
h)。。。后续匹配
根据以上讨论,对k(next[j])考虑如下定义:
(1)next[j] = 0(j=1);
(2)next[j] = Max{k|1<k<j且}(j!=1且集合有解);
(3)next[j] = 1(j!=1且集合无解);
三、求取next[j]
根据next[j]的定义可知,此函数的值取决于模式串的本身以及模式串的失配位置。此时把求解next[j]问题看成是一个模式串自匹配问题,即主串和模式串都是P,从主串P的开始与模式串P的开始进行匹配:
当j=1时,由定义得:next[1]=0;
当j!=1时,我们思路是:按照i的值由小及大,依次求next[2]、next[3]、...next[m],m为模式串的维数。现在假设已知next[j]=k,且next[t](t<j)均已求得。如果求得next[j+1]与next[j]的关系,那么所有的next函数值均可已被求出。
此时,由next[j]的定义可知:,且k为最大值,下面分两种情况讨论:
(a)如果,结合}可以得到,又不存在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 语句部分)
(b)如果,将模式串向右滑动至k'(k'=next[k]<k<j)位置,使得主串的字符与模式串的字符比较。
①如果此时(k'<k),结合,则有,由next函数的定义该式等价于:next[j+1]=k'+1=next[k]+1(观察下标k<j,由于next[t](t<=j)已知,则一定可以求出next[j+1])。
②如果此时,则将模式串继续向右滑动,直至和模式串的某个字符匹配成功,此时(k_lucky<k),结合,则有,由next函数的定义该式等价于:next[j+1]=k_lucky+1=next[...next[k]...]+1(在几次连续的滑动过程中,每次迭代k'=next[k],k'<k<j恒成立,由于next[t](t<=j)可知已知,则一定可以求出next[j+1])。
①和②的讨论说明,无论经过多少次滑动,只要主串的最终与模式串字符匹配成功,则主串字符指针(j+1)位置处的next[j+1]一定可以由某一个next[t](t<j)加1求得。
③尽管向右滑动,很不幸找不到k'使得,这相当于匹配过程中无解,此时由定义知next[j+1]=1。
故伪代码如下:
void get_next(SString P, int &next[]){ //求模式串P的next函数值并存入数组next中,i、j分别代表主串、模式串的下标 i = 1; j = 0; next[1] = 0; while(i < P[0]){ if( j ==0 || P[i] == P[j] ) { ++i; ++j; next[i] = j; }//每当自增i和j,得到一个新的next[i] else j = next[j];//模式串向右移动 } }
有以下模式串:
i: |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
11 |
12 |
13 |
14 |
15 |
16 |
|
P: |
a |
c |
a |
b |
a |
a |
b |
a |
a |
b |
c |
a |
c |
a |
a |
算法的前几次迭代过程列举如下:
a)执行if,++i,++j,进入初始状态(i=2,j=1),next[2]=1:
i: |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
11 |
12 |
13 |
14 |
15 |
16 |
|
S: |
a |
c |
a |
b |
a |
a |
b |
a |
a |
b |
c |
a |
c |
a |
a |
|
P: |
a |
c |
a |
b |
a |
a |
b |
a |
a |
b |
c |
a |
c |
a |
a |
|
j: |
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
11 |
12 |
13 |
14 |
15 |
16 |
b)执行else,模式串向右滑动(i=2,j=0):
i: |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
11 |
12 |
13 |
14 |
15 |
16 |
||
S: |
a |
c |
a |
b |
a |
a |
b |
a |
a |
b |
c |
a |
c |
a |
a |
||
P: |
a |
c |
a |
b |
a |
a |
b |
a |
a |
b |
c |
a |
c |
a |
a |
||
j: |
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
11 |
12 |
13 |
14 |
15 |
16 |
c)j==0,执行if,++i,++j(i=3,j=1),next[3]=1:
i: |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
11 |
12 |
13 |
14 |
15 |
16 |
||
S: |
a |
c |
a |
b |
a |
a |
b |
a |
a |
b |
c |
a |
c |
a |
a |
||
P: |
a |
c |
a |
b |
a |
a |
b |
a |
a |
b |
c |
a |
c |
a |
a |
||
j: |
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
11 |
12 |
13 |
14 |
15 |
16 |
i: |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
11 |
12 |
13 |
14 |
15 |
16 |
||
S: |
a |
c |
a |
b |
a |
a |
b |
a |
a |
b |
c |
a |
c |
a |
a |
||
P: |
a |
c |
a |
b |
a |
a |
b |
a |
a |
b |
c |
a |
c |
a |
a |
||
j: |
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
11 |
12 |
13 |
14 |
15 |
16 |
以此类推。。。
四、改进的next函数值算法
这样的改进已经是很不错了,但算法还可以改进,注意到下面的匹配情况:
i |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
|
S |
a |
a |
a |
b |
a |
a |
a |
a |
b |
|
P |
a |
a |
a |
a |
b |
|||||
j |
1 |
2 |
3 |
4 |
5 |
|||||
next[j] |
0 |
1 |
2 |
3 |
4 |
|||||
nextval[j] |
0 |
0 |
0 |
0 |
4 |
模式串P中的和主串S中的失配时,P向右滑动至next[4]=3位置,此时的比较还是会失配,然后P向右滑动至next[3]=2位置,此时的比较还是会失配,P向右滑动至next[2]=1位置。如果我们能够修正next[4]的值为1,使得出现类情况的时候,P能够一次性直接滑动到next[4]=1的位置,如此便可以消除这样的冗余比较。
下面对next的函数值进行修正:如果我们事先知道,上一次主串模式串字符相等 ,经过++i,++j,再一次得到,此时,i自增后的next[i]不取j,而是选取j的下一个状态next[j](由于j<i恒成立,next[j]的值均为已知的),这相当于我们在求next函数值的时候,把这些冗余的比较进行预处理,如此就可以消除模式串与主串之间这样的多余比较。
稍加改进得到:
void get_nextval(SString P, int &nextval[]){ //求模式串P的next函数修正值并存入数组nextval i = 1; j = 0; nextval[1] = 0; while(i < P[0]){ if(j == 0 || P[i] == P[j]){ ++i; ++j; if(P[i]! = P[j]) nextval[i] = j;//下次比较不相等,nextval[i]取j else nextval[i] = nextval[j];//下次比较仍然相等,nextval[i]取j的下一个nextval[j] } else j=nextval[j]; } }
五、KMP算法
求得next[j]的函数值之后,我们就可以根据KMP算法进行字符串匹配了。
在匹配过程中,如果,则i和j分别增1;否则,i不变,j移动到next[j]的位置继续比较,以此类推,直至下面两种可能:1)j退至某个next值(next[next[…next[j]...]])时字符比较相等,则指针各自增1,继续进行匹配;2)退到next[next[…next[j]...]]为0,此时需要将模式串向右滑动一个位置,即从主串的下一个字符起和模式串开始重新匹配。
KMP算法伪代码如下:
int Index_KMP(SString S, SString P, int pos){ //利用模式串P的next函数求P在主串S中第pos个字符之后的位置的KMP算法。 //其中,P非空,1≤pos≤StrLength(S)。 i = pos; j = 1; while(i <= S[0] && j <= P[0]){//S[0]以及P[0]代表字符串长 if(j == 0 || S[i] == P[j]) { ++i; ++j; }//继续比较后继字符 else j = next[j];//模式串象右滑动 } if(j > P[0]) return i - P[0];//匹配成功 else return 0; }//Index_KMP
以下主串和模式串的匹配过程如图所示:
i: |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
11 |
12 |
13 |
14 |
15 |
16 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
六、直接看出next[j]函数值(这部分有待考证)
根据以上next[j]的定义:
(1)next[j] = 0(j=1);
(2)next[j] = Max{k|1<k<j且}(j!=1且集合有解);
(3)next[j] = 1(j!=1且集合无解);
可以直接看出模式串的next函数值。这主要遵循2条规则:
a)如果上一个next[j]!=1,按照next[j] = Max{k|1<k<j且}的规则求解下一个next[j+1]的值;
b)如果上一个next[j]=1,由next[j]的求解算法知,此时主串的第j位与模式串的第1位对齐,比较这两位:如果不等,则主串的第j+1位与模式串的第1位再对齐,故next[j+1]=1;如果相等,则next[j+1]=Max{k|1<k<j且}
举例如下:
P |
a |
b |
a |
a |
b |
a |
a |
a |
b |
a |
j |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
next[j] |
0 |
1 |
1 |
2 |
2 |
3 |
4 |
5 |
2 |
3 |
由定义知next[1]=0;
由j!=1且集合无解知next[2]=1;
由于next[2]=1,此时i=2,j=1,令这两个字符对齐,发现这两个字符不相等,则next[3]=1;
由于next[3]=1,此时i=3,j=1,令这两个字符对齐,发现这两个字符相等,根据next[j+1]=Max{k|1<k<j且}=2,故next[4]=2;
P(1之后串头) |
a |
b |
a |
a |
b |
a |
a |
a |
b |
a |
P(j之前串尾) |
a |
b |
a |
a |
b |
a |
a |
a |
b |
a |
j |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
next[j] |
2 |
由于有1个匹配,故next[5]=2;
P(1之后串头) |
a |
b |
a |
a |
b |
a |
a |
a |
b |
a |
P(j之前串尾) |
a |
b |
a |
a |
b |
a |
a |
a |
b |
a |
j |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
next[j] |
2 |
由于有2个匹配,故next[6]=3;
P(1之后串头) |
a |
b |
a |
a |
b |
a |
a |
a |
b |
a |
P(j之前串尾) |
a |
b |
a |
a |
b |
a |
a |
a |
b |
a |
j |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
next[j] |
3 |
。。。后续匹配
这个文章思路是我理解KMP的思路,内容可能显得比较啰嗦,但是很具体,希望对大家有所帮助。
第一次写文章,还请大家多多提意见。
参考文献:《数据结构》,严蔚敏著