算法导论-字符串匹配
2017-02-12 23:21
190 查看
编辑文本时,我们经常需要在文本中找到某串模式字符在整个文本中出现的位置,这个模式字符串即为用户查找输入的关键字,解决这个问题的算法为字符串匹配算法。
当我们遇到这个问题,如何查找在文本中出现的模式呢?
一:朴素字符串匹配
我们假设存在两个游标i,j分别指向文本串与模式串的位置,那么有
1:当匹配到T[i]==P[j],则i++,j++;
2:当在匹配到某一位置时出现T[i]!=P[j]时,即匹配失败,此时i回溯到本次开始匹配的位置,j=0
咱们从代码与匹配图解理解朴素算法思路:
OK,现在我们举个栗子,
假设有一个文本字符串和模式字符串如下图
(a) 字符串开始匹配,此时T[0]=P[0],i++为1,此时s+i=1,T[1]!=P[1],匹配失败,s自加1,变为1,开始图(b)的匹配过程;
(b) 此时T[s=1]=c!=P[0],匹配失败,s自加1,变为图c的匹配过程
(c) 此刻T[s=2]=P[0],i自加1,T[s+i=3]=P[i=1],字符相等,继续下一匹配,i自加1为2,T[s+i=4]=P[i=2],且此刻i=length(P),匹配结束。此刻,s继续自加,进行余下字符串的匹配。
朴素字符串匹配过程较简单,但是最坏情况下时间复杂度为O((n-m+1)*m),字符串的匹配时间开销较大,并不能很好的解决字符串的匹配。
补充:
在朴素字符串匹配中存在一种特殊情况,即模式串P中的所有字符都不相同,其匹配时间可以达到O(n),具体的实现代码如下:
这个实现过程很好理解,下面图示匹配过程:
整个匹配流程如下:
(1) 文本串i=0,模式串j=0,此时T[i]!=P[j],匹配失败,i自加1;
(2) 开始匹配,T[1-4]=P[0-3],但T[5]!=P[4],匹配失败,由于P中所有字符互不相同,所以i不必回溯到T[i=2],只需开始匹配T[i-1=4]与P[0]并开始新一轮的匹配过程。
至此,朴素字符串匹配算法结束,整个过程很好理解。
二Rabin-Karp算法
以10进制数来表示字符,例如笔者用0表示a,t同理1表示b,以此类推,那么字符串dbebf可以表示为31415,将字符串看做数字,在算法匹配中更加方便。
给定一个模式串P[1-m],同理在文本串T[s+1..s+m]表示文本串中某段与模式串长度相等的一串字符,那么分别用数字p,t表示这两个字符串,通过比较数字p和t的大小,就可确定字符串是否匹配而无需用模式串中字符依次匹配文本串。示例图如下:
算法的思想理解之后,我们需要思考如何计算p和t.
数学中有霍纳法则,我们运用霍纳法则在O(m)内计算p:
p=P[m]+10(P[m-1]+10(P[m-2]+…+10(P[2]+10P1)…)))
霍纳法则的解释如下:
运用霍纳法则,类似的我们也可以根据T[s+1…s+m]计算出t.
但为了节约时间,我们可以利用一下方法在常数时间内根据ts,计算出ts+1.具体过程如下图解:
如图所示,ts=31415,ts+1=14152,则
ts+1=(ts-(T[s+1]=3)*10^(m=4))*10+(T[s+m+1]=2)
注:(ts-(T[s+1]=3)*10^(m=4))=31415-30000=1415
即 ts+1=10(ts-10^(m-1)T[s+1])+T[s+m+1]
在计算过程中,可能会出现p与t的值过大,可以取模运算。
下面的代码实现了上述的思想:
三:有限自动机
有限自动机:每读入字符串的一个字符,则其状态从当前q0转变为q(a)。
对于给定的的模式P=ababaca,首先定义一个函数f()为P的后缀函数,满足f(x)是x的后缀是P的最长前缀,
举个栗子:
f(ccaca)=1 x=ccaca,是x的后缀同时是P的最长前缀的是字符a,所以
f(ccaca)=1
同理f(ccab)=2
定义转移函数g(q,a):q为任意状态和字符a,则定义如下
g(q,a)=f(Pqa);记录当前状态q与当前字符a时已得到的与模式P匹配的文本字符串T的最长前缀。
原理:
自动机处于状态q并且读入下一个字符T[i+1]=a,那么此时这个状态转换是Tia的后缀,同时是P的最长前缀,记为f(Tia)。
实例解释:
算法导论中例子:
T=abababacaba,P=ababaca
此时T中的字符为a,b,c这三种,那么对于状态从0开始,加入从T中读入a,则f(T1a)=1,T1a=a,T1a的后缀为P的最长前缀=a;假如读取字符为b/c,
则T1b/c的后缀为b/c,同时又是P的最长前缀的字符没有,状态为0
所以state 0=1,0,0
对于state=1,从T2中读取a,则T2a=aa,是T2a的后缀同时又是P的最长前缀=a,f(T2a)=1;假如读取字符为b,则T2a=ab,是T2a的后缀同时又是P的最长前缀=ab,所以f(T2a=b)=2,同理假如读取字符为c,则ac后缀与P的最长前缀为空,f(T2a=c)=0
依次推算,可以理解状态转移表中内容。
由上可知有两种情况:
第一种情况 a=P[q+1],使字符a可以继续匹配,那么可以沿着自动机主线(图中灰色部分)继续进行
第二种情况:a!=P[q+1],此时我们需要找到一个更小的子串,满足它是Tia的后缀同时是P的最长前缀。如f(5,b)=4,是因为状态q=5时读取字符b
此时T5a=ababab,P=ababaca,是T5a的后缀同时又是P的最长前缀=abab,所以f(5,b)=4.
对于Tia的后缀,同时是P的最长前缀下图可以理解
设Tia=ababcab P=abaca
即Tia从后往前与P的最大交集(这样解释虽然不科学)
理解了这个过程,则程序如下:
四:KMP算法
理解KMP算法,首先得理解PI数组的作用。
以朴素字符串的匹配过程为例:
如图所示:在模式P匹配文本T时,当匹配到最后一位不匹配时,朴素字符串匹配的做法是P往前移动一位继续匹配:
可是根据我们的观察发现更有效的做法是P可以直接往前移动两位,如下
如上个人理解PI数组主要记录当前匹配无效时下次的有效偏移位数,避免无效的偏移。
KMP算法充分利用已匹配的信息,避免重复的匹配过程。
计算PI数组需要有效自动机中前缀与后缀的概念。
对于字符串abcd,则d,cd,bcd为其后缀,a,ab,abc为其前缀。
KMP的理解点一在于 PI 数组的求解,二在于利用PI数组进行匹配
PI数组的求解:
算法导论中伪代码如上:
对于PI数组的值可以理解为前后缀的匹配,在上述伪代码中,对于PI[1]=0,是因为一个字符,既无后缀,又无前缀。
从5-10行for循环的迭代开始,while循环搜索k(PI数组)的值,直至找到一个k的值,使得P[k+1]=P[q]。
(由算法导论引理32.5与引理32.6可知,k必定在转移函数PI数组中)
8-9行代码实现了PI值得调整,取得前缀是其最大后缀。(引理32.7)。
KMP算法的精髓如下:
KMP的伪代码如下:
这两段代码思想完全相同,如果和前缀不同就比较前缀的前缀
基于C语言实现如下:
当我们遇到这个问题,如何查找在文本中出现的模式呢?
一:朴素字符串匹配
我们假设存在两个游标i,j分别指向文本串与模式串的位置,那么有
1:当匹配到T[i]==P[j],则i++,j++;
2:当在匹配到某一位置时出现T[i]!=P[j]时,即匹配失败,此时i回溯到本次开始匹配的位置,j=0
咱们从代码与匹配图解理解朴素算法思路:
int native_string_matcher(char* T,char* P) { //获取原始串和模式串字符长度 int n=strlen(T); int m=strlen(P); int s=0,i=0; //原串开始从0至n-m偏移,以匹配模式串 for(s=0;s<=n-m;s++) { //模式串从0-m开始分别匹配模式串中字符是否与原串相等 for(i=0;i<m;i++) { if(P[i]!=T[s+i]) //如果在匹配过程中有字符不相等,则跳出该循环,偏移S向下移位,继续重新匹配 { break; } if(i==m-1)//当i=m-1,且P最后字符与T最后字符相等,则表示字符串匹配成功,此时返回原串中与模式串相匹配的起始位置。 printf("has match"); return s; } } }
OK,现在我们举个栗子,
假设有一个文本字符串和模式字符串如下图
(a) 字符串开始匹配,此时T[0]=P[0],i++为1,此时s+i=1,T[1]!=P[1],匹配失败,s自加1,变为1,开始图(b)的匹配过程;
(b) 此时T[s=1]=c!=P[0],匹配失败,s自加1,变为图c的匹配过程
(c) 此刻T[s=2]=P[0],i自加1,T[s+i=3]=P[i=1],字符相等,继续下一匹配,i自加1为2,T[s+i=4]=P[i=2],且此刻i=length(P),匹配结束。此刻,s继续自加,进行余下字符串的匹配。
朴素字符串匹配过程较简单,但是最坏情况下时间复杂度为O((n-m+1)*m),字符串的匹配时间开销较大,并不能很好的解决字符串的匹配。
补充:
在朴素字符串匹配中存在一种特殊情况,即模式串P中的所有字符都不相同,其匹配时间可以达到O(n),具体的实现代码如下:
int native_string_matcher(char* T,char* P) { //获取原始串和模式串字符长度 int n=strlen(T); int m=strlen(P); int j=0,i; for(i=0,j=0;i<n;i++) { if(T[i]==P[j]&&j<m) ++j; else{ if(j!=0) { j=0; --i; } } printf("%d\n",i); if(j==m) return i-m+1; } return -1;//若为-1,代表原始串中不包含模式串 }
这个实现过程很好理解,下面图示匹配过程:
整个匹配流程如下:
(1) 文本串i=0,模式串j=0,此时T[i]!=P[j],匹配失败,i自加1;
(2) 开始匹配,T[1-4]=P[0-3],但T[5]!=P[4],匹配失败,由于P中所有字符互不相同,所以i不必回溯到T[i=2],只需开始匹配T[i-1=4]与P[0]并开始新一轮的匹配过程。
至此,朴素字符串匹配算法结束,整个过程很好理解。
二Rabin-Karp算法
在实际应用中,Rabin-Karp算法的预处理时间为O(m),并且在最坏的情况下的时间复杂度为O((n-m+1)m),相对于朴素字符串,它的运行时是比较好的。整个算法思想介绍如下:
以10进制数来表示字符,例如笔者用0表示a,t同理1表示b,以此类推,那么字符串dbebf可以表示为31415,将字符串看做数字,在算法匹配中更加方便。
给定一个模式串P[1-m],同理在文本串T[s+1..s+m]表示文本串中某段与模式串长度相等的一串字符,那么分别用数字p,t表示这两个字符串,通过比较数字p和t的大小,就可确定字符串是否匹配而无需用模式串中字符依次匹配文本串。示例图如下:
算法的思想理解之后,我们需要思考如何计算p和t.
数学中有霍纳法则,我们运用霍纳法则在O(m)内计算p:
p=P[m]+10(P[m-1]+10(P[m-2]+…+10(P[2]+10P1)…)))
霍纳法则的解释如下:
运用霍纳法则,类似的我们也可以根据T[s+1…s+m]计算出t.
但为了节约时间,我们可以利用一下方法在常数时间内根据ts,计算出ts+1.具体过程如下图解:
如图所示,ts=31415,ts+1=14152,则
ts+1=(ts-(T[s+1]=3)*10^(m=4))*10+(T[s+m+1]=2)
注:(ts-(T[s+1]=3)*10^(m=4))=31415-30000=1415
即 ts+1=10(ts-10^(m-1)T[s+1])+T[s+m+1]
在计算过程中,可能会出现p与t的值过大,可以取模运算。
下面的代码实现了上述的思想:
#include"stdio.h" #include"string.h" #include"math.h" char* substr(char* p,int start,int end) { char *q=malloc(sizeof(char)*(end-start+1)); int i; for(i=0;i<end-start+1;i++) *(q+i)=*(p+start+i); // *(q+end-start+1)='\0'; return q; }//取T中T[s+1,s+m]子字符串 void RABIN_KARP_MATCHER(char T[],char P[],int d,int q) { int n,m,h,p,t,i,t0=0; n=strlen(T); m=strlen(P); h=((int)pow(d,m-1)); // h=((int)pow(d,m-1))%q;//取模运算 p=0; t=0; for(i=0;i<m;i++) { p=(d*p+P[i]-'a')%q;//求p t =(d*t +T[i]-'a')%q; t0=(d*t0 +T[i]-'a');//求初始t0=num(T[1,m]) } int s; for(s=0;s<=n-m;s++) { if(p==t&&(strcmp(P,substr(T,s,s+m-1))==0)) // if(p==t) { printf("%d\n",s); } printf("%d %d %c \n",t,p,T[s]); int tem=t0; t0=(d*(tem-(T[s]-'a')*h)+T[s+m]-'a');//求ts+1; t=t0%q; // int tem=t; // t=(d*(tem-(T[s]-'a')*h)+T[s+m]-'a')%q; } } int main() { char T[100], P[100]; scanf("%s",T); scanf("%s",P); RABIN_KARP_MATCHER(T,P,10,13); return 0; }
三:有限自动机
有限自动机:每读入字符串的一个字符,则其状态从当前q0转变为q(a)。
对于给定的的模式P=ababaca,首先定义一个函数f()为P的后缀函数,满足f(x)是x的后缀是P的最长前缀,
举个栗子:
f(ccaca)=1 x=ccaca,是x的后缀同时是P的最长前缀的是字符a,所以
f(ccaca)=1
同理f(ccab)=2
定义转移函数g(q,a):q为任意状态和字符a,则定义如下
g(q,a)=f(Pqa);记录当前状态q与当前字符a时已得到的与模式P匹配的文本字符串T的最长前缀。
原理:
自动机处于状态q并且读入下一个字符T[i+1]=a,那么此时这个状态转换是Tia的后缀,同时是P的最长前缀,记为f(Tia)。
实例解释:
算法导论中例子:
T=abababacaba,P=ababaca
此时T中的字符为a,b,c这三种,那么对于状态从0开始,加入从T中读入a,则f(T1a)=1,T1a=a,T1a的后缀为P的最长前缀=a;假如读取字符为b/c,
则T1b/c的后缀为b/c,同时又是P的最长前缀的字符没有,状态为0
所以state 0=1,0,0
对于state=1,从T2中读取a,则T2a=aa,是T2a的后缀同时又是P的最长前缀=a,f(T2a)=1;假如读取字符为b,则T2a=ab,是T2a的后缀同时又是P的最长前缀=ab,所以f(T2a=b)=2,同理假如读取字符为c,则ac后缀与P的最长前缀为空,f(T2a=c)=0
依次推算,可以理解状态转移表中内容。
由上可知有两种情况:
第一种情况 a=P[q+1],使字符a可以继续匹配,那么可以沿着自动机主线(图中灰色部分)继续进行
第二种情况:a!=P[q+1],此时我们需要找到一个更小的子串,满足它是Tia的后缀同时是P的最长前缀。如f(5,b)=4,是因为状态q=5时读取字符b
此时T5a=ababab,P=ababaca,是T5a的后缀同时又是P的最长前缀=abab,所以f(5,b)=4.
对于Tia的后缀,同时是P的最长前缀下图可以理解
设Tia=ababcab P=abaca
即Tia从后往前与P的最大交集(这样解释虽然不科学)
理解了这个过程,则程序如下:
#include<stdio.h> #include <cstring> #define min(x,y) (x<y?x:y) #define MAX_LEN 100 #define MAX_CHAR 4 int state[MAX_LEN][MAX_CHAR]; int Prefix_cmp(char *P,int k,int q, char a) //求后缀 { if(k==0) return 1; if(k==1) { return P[k-1]==a; } return P[k-1]==a&&(strncmp(P,P+q-k+1,k-1)==0); } void COMPUTE_TRANSITION_FUNCTION(char *P,char *A)//计算转移函数 { int m,q,k,i; m = strlen(P); k = 0; printf("%d \n",m); for(q=0;q<=m;++q) { for(i=0;i<MAX_CHAR-1;++i) { k=min(m+1,q+2); do { k=k-1; }while(!Prefix_cmp(P,k,q,A[i]));//循环直至找出k,使Pk是(Pq+a)的最大后缀 state[q][i]=k; printf("%d|%d ",k,q); printf("%s\n",P); } } printf("\n"); for(i=0;i<=m;++i) { for(q=0;q<MAX_CHAR-1;q++) printf("(%d)%d ",i,state[i][q]); printf("\n"); } } void FINITE_AUTOMATON_MATCHER(char *T,char *P,char *A) //根据转移函数匹配字符串 { int n,m,i,q; n=strlen(T); m=strlen(P); q=0; COMPUTE_TRANSITION_FUNCTION(P,A); for(i=0;i<n;i++) { q=state[q][T[i]-'a']; if(q==m) printf("%d\n",i-m); } } int main() { char T[MAX_LEN],P[MAX_LEN],A[MAX_CHAR]; scanf("%s",A); scanf("%s",P); scanf("%s",T); FINITE_AUTOMATON_MATCHER(T,P,A); return 0; }
四:KMP算法
理解KMP算法,首先得理解PI数组的作用。
以朴素字符串的匹配过程为例:
如图所示:在模式P匹配文本T时,当匹配到最后一位不匹配时,朴素字符串匹配的做法是P往前移动一位继续匹配:
可是根据我们的观察发现更有效的做法是P可以直接往前移动两位,如下
如上个人理解PI数组主要记录当前匹配无效时下次的有效偏移位数,避免无效的偏移。
KMP算法充分利用已匹配的信息,避免重复的匹配过程。
计算PI数组需要有效自动机中前缀与后缀的概念。
对于字符串abcd,则d,cd,bcd为其后缀,a,ab,abc为其前缀。
KMP的理解点一在于 PI 数组的求解,二在于利用PI数组进行匹配
PI数组的求解:
算法导论中伪代码如上:
对于PI数组的值可以理解为前后缀的匹配,在上述伪代码中,对于PI[1]=0,是因为一个字符,既无后缀,又无前缀。
从5-10行for循环的迭代开始,while循环搜索k(PI数组)的值,直至找到一个k的值,使得P[k+1]=P[q]。
(由算法导论引理32.5与引理32.6可知,k必定在转移函数PI数组中)
8-9行代码实现了PI值得调整,取得前缀是其最大后缀。(引理32.7)。
KMP算法的精髓如下:
KMP的伪代码如下:
这两段代码思想完全相同,如果和前缀不同就比较前缀的前缀
基于C语言实现如下:
#include<stdio.h> #include<string.h> #define MAX_LEN 20 int PI[MAX_LEN]; void Compute_prefix_fun(char P[]) { int m,k,q; m=strlen(P); PI[0]=0; k=0; for(q=1;q<m;++q) { while((k>0)&&P[k]!=P[q]) { //q=PI[q]; k=PI[k]; } if(P[k]==P[q]) k=k+1; PI[q]=k; } } void KMP_matcher(char T[],char P[]) { int n,m,q,i; n=strlen(T); m=strlen(P); Compute_prefix_fun(P); q=0; for(i=0;i<n;i++) { while((q>0)&&P[q]!=T[i]) { q=PI[q-1]; } if(P[q]==T[i]) q=q+1; if(q==m) { printf("%d\n",i-m+1); q=PI[q-1]; } } } int main() { char T[20],P[20]; scanf("%s",T); scanf("%s",P); KMP_matcher(T,P); return 0; }
相关文章推荐
- 算法导论-第32章-字符串匹配:有限自动机方法(改进版本:预处理阶段复杂度为O(m*| ∑ |) )C++实现
- 算法导论 KMP字符串匹配
- 算法导论之字符串匹配
- 算法导论-第32章-字符串匹配:Rabin-Karp算法C++实现
- 算法导论第32章 字符串匹配
- "《算法导论》之‘字符串’":字符串匹配
- 算法导论-第32章-字符串匹配:Knuth-Morris-Pratt(KMP)算法C++实现
- 朴素字符串匹配——算法导论
- 算法导论-第32章- 字符串匹配 - KMP算法
- 算法导论第三十二章-字符串匹配-Cpp代码实现
- 洛谷P3375 - 【模板】KMP字符串匹配
- [算法学习]字符串匹配
- 算法——蛮力法之顺序查找和蛮力字符串匹配
- CODE[VS] 1404 字符串匹配
- 字符串匹配算法 – Sunday算法
- HDU6208 The Dominator of Strings 字符串匹配 2017 ACM/ICPC Asia Regional Qingdao Online
- 字符串匹配——KMP算法
- 使用GPU进行字符串匹配--cuda编程实现
- kmp字符串匹配算法
- [LeetCode-28] Implement strStr(KMP 字符串匹配)