您的位置:首页 > 其它

字符串匹配——简单匹配,KMP,分析讲解

2016-10-31 15:33 363 查看

KMP算法详解

1.问题描述

有两个字符串s跟p

其中

s=abcabcaabca

p=abcaab

设计算法判断p是否为s子字符串,若是,输出yes和p字符串在s中的起始位置下标,

否则输出no

2.简单匹配算法的不足

简单匹配算法通常有3个指针i、j、pos , i是s串指针,j是p串指针,pos用来记录i的回溯位置。下面给出过程图:

pos
i
↓
s= a b c a b c a a b c a
p= a b c a a b
↑
j


s[0],p[0]匹配成功,下一步i、j右移一位

pos i
↓↓
s= a b c a b c a a b c a
p= a b c a a b
↑
j


s[1],p[1]匹配成功,下一步i、j右移一位

pos   i
↓  ↓
s= a b c a b c a a b c a
p= a b c a a b
↑
j


s[2],p[2]匹配成功,下一步i、j右移一位

……同样s[3],p[3]匹配成功,下一步i、j右移一位

pos       i
↓      ↓
s= a b c a b c a a b c a
p= a b c a a b
↑
j


s[4],p[4]匹配失败,下一步pos右移一位,i回溯到pos,j回溯到0,如步骤⑤所示

pos
i
↓
s= a b c a b c a a b c a
p=   a b c a a b
↑
j


s[1],p[0]匹配失败,下一步pos右移一位,i回溯到pos,j回溯到0,如步骤⑥所示

pos
i
↓
s= a b c a b c a a b c a
p=     a b c a a b
↑
j


s[2],p[0]匹配失败,下一步pos右移一位,i回溯到pos,j回溯到0,如步骤⑦所示

pos
i
↓
s= a b c a b c a a b c a
p=       a b c a a b
↑
j


s[3],p[0]匹配成功,下一步i、j右移一位,如步骤⑧所示

pos i
↓↓
s= a b c a b c a a b c a
p=       a b c a a b
↑
j


s[3],p[0]匹配成功,下一步i、j右移一位

就这样一直匹配到p串最右一位

pos         i
↓        ↓
s= a b c a b c a a b c a
p=       a b c a a b
↑
j


此时p串最后一个字符匹配成功,i、j自增1后,判断if( j==strlen(p) ),判定为s与p匹配成功。

此时我们总结一下,简单匹配算法中是存在很多不必要的回溯的,在到达第一次失配点后,完全可以直接进行s[3]和p[0]的比较,如下图所示。

i
↓
s= a b c a b c a a b c a
p= a b c a a b
↑
j

i
↓
s= a b c a b c a a b c a
p=       a b c a a b
↑
j


这既是简单匹配的不足,又是KMP算法的精髓所在。到达失配点后,i指针不回溯,j指针不一定回溯到0,从而避免了大量的不必要的匹配过程。

3.KMP算法中j指针回溯的问题

上面一节已经说到,KMP算法中i指针不回溯,j指针不一定回溯到0 。 那么i指针不回溯好说,j指针不回溯到0,应该回溯到哪呢,下面给出几个示例来帮助大家理解:

回溯到1

i
↓
s= a b c a b c a a b c a
p= a b c a a b
↑
j

i
↓
s= a b c a b c a a b c a
p=       a b c a a b
↑
j


回溯到2

i
↓
s= a b c a b c a b c
p= a b c a b d
↑
j

i
↓
s= a b c a b c a b c
p=       a b c a b d
↑
j


回溯到0

i
↓
s= a b c d a b c d a b c
p= a b c a b c
↑
j

i
↓
s= a b c d a b c d a b c
p=       a b c a b c
↑
j


看完上面3个j指针回溯的例子,相信大家也有所感悟,下面我们来具体分析一下j指针的回溯:

首先看上面的回溯到1的例子,为了观看方便我把图复制到下面了:

i
↓
s= a b c a b c a a b c a
p= a b c a a b
↑
j

i
↓
s= a b c a b c a a b c a
p=       a b c a a b
↑
j


此时为什么j指针是回溯到1呢,我们可以这样去理解:

首先因为s串和p串的前4个字符已经匹配成功,所以s[0]=p[0],s[1]=p[1],s[2]=p[2],s[3]=p[3],对,就是这里!有个s[3]=p[3],什么意思呢?我们通过比较得出s[3]=p[3]的结论,而如果我们在比较之前就告诉程序p[0]是等于p[3]的,那程序不就可以判断出s[3]是等于p[0]嘛!

可能看到这里大家还会不太明白,那我们再看一下剩下的两个例子

以下是j回溯到2的例子

i
↓
s= a b c a b c a b c
p= a b c a b d
↑
j

i
↓
s= a b c a b c a b c
p=       a b c a b d
↑
j


由于第一轮的匹配程序可以知道s串前5个字符和p串的前5个字符是分别相等了,也就是说s[3]=p[3],s[4]=p[4]。如果我们在程序运行前就告诉它,p[0]=p[3],p[1]=p[4],即p串的里面两个ab相等,那么程序不就可以判断出s[3]=p[0],s[4]=p[1]了嘛,所以下一轮只需要比较s[5]和p[2]

下面是回溯到0的例子:

i
↓
s= a b c d a b c d a b c
p= a b c a b c
↑
j

i
↓
s= a b c d a b c d a b c
p=       a b c a b c
↑
j


由于已经匹配成功的前3个字符中,前后没有相等的子串,或者说前后相等子串的最大长度为0,所以j只能回溯到0

相信看了这么多例子,大家对j指针回溯也有了一定的理解。不难看出:

j指针回溯位置与 它当前指向位置之前的子串中的相等前后缀的长度 有关。

而next数组就是用来记录j指针回溯位置的。

下面我们来看一下next数组的定义:

next[i]表示p[0]p[1]…p[i-1]串中的最长的相等的前后缀的长度

比如 abcaba 中 next[3]=0,因为adc中没有相等的前后缀,next[4]=1,因为adca中前后有a=a,next[5]=2,因为abcab中前后有ab=ab 。是不是很好理解呢。

有了next数组定义,我们再来分析一下j指针回
4000
溯。不难看出,j指针的回溯就是j=next[j]即可。

4.如何求next数组

①简单分析

我们就拿本文问题描述中的字符串来分析:

p=abcaab

好,首先我们直接采取用眼睛观察的方式来口算一下,额不对,是眼算一下next数组的值

next[0]=0

next[1] 只有a,就一个字符,所以也是next[1]=0

next[2] ab ,没有前后相等的前后缀,所以next[2]=0

next[3] abc,也没有相等的前后缀,所以next[3]=0

next[4] abca,诶,a=a,太好了,所以next[4]=1

next[5] abcaa,还是只有a=a,所以next[5]=1

好,到现在为止next数组就可以算完了,口算是不是非常简单呢?我们把p字符串和next数组对应放在一起

p= a b c a a b
next= 0 0 0 0 1 1


这样可以清楚的看出每次匹配失败后j指针的回溯位置。

我们通过观察就可以得出next数组的结果,但问题是怎样用程序来计算next数组呢?下面我们来分析:

首先next[0]=next[1]=0这是固定的,因为next[0]表示空字符串的相等前后缀长度,而next[1]表示单个字符p[0]的相等前后缀长度,一个字符谈不上前后缀,所以为0 。 其他的next值则需要程序去计算。指针为u,v

计算next[2]

next= 0 0 ↓
p= a b c a a b
↑↑
u v


p[u]!=p[v]所以next[2]=0

计算next[3],next[3]可能等于2吗?是不可能的,因为由next[2]=0得到p[0]!=p[1],那么一定有p[0]p[1]!=p[1]p[2],所以next[3]不可能等于2,只能等于0或1

next= 0 0 0 ↓
p= a b c a a b
↑  ↑
u   v


p[u]!=p[v]所以next[3]=0

计算next[4],同上next[4]只能等于0或1

next= 0 0 0 0 ↓
p= a b c a a b
↑    ↑
u     v


p[u]==p[v]所以next[4]=1

计算next[5],因为next[4]=1,所以next[5]是可能等于2的。前面已经判断出p[0]=p[3],所以我们要判断next[5]是否等于2,只需要比较p[1]和p[4]即可,若p[1]==p[4],那么就有p[0]p[1]==p[3]p[4],next[5]就会等于2;若p[1]!=p[4],那么p[0]p[1]!=p[3]p[4],next[5]就不等于2。则所以u指针右移一位,先检查next[5]是否等于2

next= 0 0 0 0 1 ↓
p= a b c a a b
↑    ↑
u     v


p[u]!=p[v]所以next[5]!=2 。下一步u=next[u],即u=next[1]=0,再次比较

next= 0 0 0 0 1 ↓
p= a b c a a b
↑      ↑
u       v


p[u]=p[v],所以next[5]=1,至此next数组都求完了

next= 0 0 0 0 1 1
p= a b c a a b


②求next数组时的回溯

大家有没有发现上面有个地方没有讲明白?是的!为什么在求next数组的时候,会有u=next[u]的回溯呢?为了说明这个问题,我们需要一个很长很变态的p字符串来分析。于是笔者就在这里放了一个又长又变态的p 字符串,来帮助大家理解。

No.   0  1  2  3  4  5  6  7  8  9  10 11 12 13 14
next  0  0  0  0  1  2  3  0  1  2  3  4  5  6  0
p= a  b  c  a  b  c  d  a  b  c  a  b  c  b  a


这里No.为数组下标,挨个数起来会死人的,为了大家的身心健康我就直接写出来了,next数组已经通过①中的算法求出,下面我们来分析一下为什么会有u=next[u]的回溯,例如此时我们还有next[12],next[13],next[14]没有求完,现在我们从next[12]开始求

No.   0  1  2  3  4  5  6  7  8  9  10 11 12 13 14
next  0  0  0  0  1  2  3  0  1  2  3  4  ↓
p= a  b  c  a  b  c  d  a  b  c  a  b  c  b  a
↑                   ↑
u                    v


求next[12]要先看next[11]

由于next[11]==4,说明了p[0]p[1]p[2]p[3]==p[7]p[8]p[9]p[10],所以我们首先要判断next[12]是否等于5,即比较p[4]和p[11]的值,由于p[4]==p[11]所以p[12]=5

接下来求next[13],首先判断next[13]是否等于6

No.   0  1  2  3  4  5  6  7  8  9  10 11 12 13 14
next  0  0  0  0  1  2  3  0  1  2  3  4  5  ↓
p= a  b  c  a  b  c  d  a  b  c  a  b  c  b  a
↑                   ↑
u                    v


我们比较p[5]和p[12]的值,由于都是c,所以next[13]=6,接下来求next[14]

No.   0  1  2  3  4  5  6  7  8  9  10 11 12 13 14
next  0  0  0  0  1  2  3  0  1  2  3  4  5  6  ↓
p= a  b  c  a  b  c  d  a  b  c  a  b  c  b  a
↑                   ↑
u                    v


首先判断是否等于7,于是我们比较p[6]和p[13],由于p[6]!=p[13]所以next[14]!=7,就是这个时候!会发生u=next[u]的回溯!

u=next[u]==next[6]==3,所以接下来要比较p[3]和p[13],如下图

No.   0  1  2  3  4  5  6  7  8  9  10 11 12 13 14
next  0  0  0  0  1  2  3  0  1  2  3  4  5  6  ↓
p= a  b  c  a  b  c  d  a  b  c  a  b  c  b  a
↑                            ↑
u                             v


从图中我们可以直观的看出这样回溯是非常有道理的,即确定next[14]!=7后,判断next[14]是否等于4即可。这个4是因为u指针指向位置之前的包括当前位置的字符串为 abca,长度为4。思考为什么会这样回溯呢。

我们来分析一下。

由next[13]==6可知:

p[0]p[1]p[2]p[3]p[4]p[5]是等于p[7]p[8]p[9]p[10]p[11]p[12]的;

由next[ next[13] ] 即next[6]==3可知:

p[0]p[1]p[2]是等于p[3]p[4]p[5]的。

那么p[7]p[8]p[9]也是等于p[10]p[11]p[12]的,可以推出来:

p[0]p[1]p[2]等于p[10]p[11]p[12]

所以我们在判定next[14]!=7后,会进行u=next[u]的回溯,即判定next[14]是否等于4

由上图可知p[3]!=p[13],所以next[14]!=4,u=next[u]=0,下一步比较p[0]和p[13],判断next[14]是否等于1 。 这个1也是因为u指针指向位置之前的包括当前位置的字符串为 a,长度为1。

No.   0  1  2  3  4  5  6  7  8  9  10 11 12 13 14
next  0  0  0  0  1  2  3  0  1  2  3  4  5  6  ↓
p= a  b  c  a  b  c  d  a  b  c  a  b  c  b  a
↑                                     ↑
u                                      v


由于p[0]!=p[13]所以next[4]只好等于0了

这里有一篇别人的求next数组博客推荐给大家,他的图画的非常好http://blog.csdn.net/yutianzuijin/article/details/11954939/

5.KMP算法代码示例(C++)

#include<iostream>
#include<string.h>
using namespace std;

int main()
{
char s[20]="abcabcaabca";
char p[10]="abcaab";
int slen=strlen(s);
int plen=strlen(p);
int next[7];
//求next数组
int i=0;
int j=0;
next[0]=0;
next[1]=0;
for(i=1;i<plen;i++)
{
while(p[i]!=p[j]&&j>0)
j=next[j];
if(p[i]==p[j]) j++;
next[i+1]=j;
}
for(i=0;i<plen;i++)
cout<<"next"<<i<<"="<<next[i]<<endl;
//KMP匹配
i=0;
j=0;
while(i<slen&&j<plen)
{
if(s[i]==p[j])
{
i++;
j++;
}
else
{
if(j==0)i++;
else j=next[j];
}
}
if(j==plen)cout<<"yes,i-j="<<i-j<<endl;
else cout<<"no"<<endl;
return 0;
}


结束

参考资料:http://blog.csdn.net/yutianzuijin/article/details/11954939/
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息