字符串匹配——简单匹配,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,应该回溯到哪呢,下面给出几个示例来帮助大家理解:①
回溯到1i ↓ 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
②
回溯到2i ↓ 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
③
回溯到0i ↓ 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/相关文章推荐
- 几种字符串匹配的方法,以及一个讲解的很清晰的KMP匹配
- 史上最浅显易懂的KMP算法讲解:字符串匹配算法
- 简单讲解KMP单模式匹配与AC算法多模式匹配(KMP篇)
- 字符串匹配算法(暴力匹配和KMP)
- 第3次CCF-3-字符串匹配(kmp的简单应用)
- P3375 【模板】KMP字符串匹配(全程注释,简单易懂)
- 【字符串匹配】KMP(implement strStr()), 正则匹配(Wildcard Matching),2-dim 动规(regular expression)
- 【字符串匹配】——KMP(看毛片算法)——深入讲解next数组的求解
- 字符串匹配暴力匹配法和KMP匹配算法对比
- KMP 字符串匹配算法讲解
- 字符串匹配的KMP算法(简单清晰的认识KMP)
- C++版字符串匹配算法之传统匹配算法加KMP字符串匹配算法
- KMP字符串匹配 简单理解
- KMP字符串匹配C++代码实现
- 字符串匹配(kmp)
- 仿写类似strstr()字符串匹配的二进制匹配方法FindBinaray()
- LDD3笔记——字符设备驱动简单分析
- greta一些简单实用的字符串匹配
- KMP字符串匹配算法笔记
- [时空权衡]字符串匹配算法 KMP