KMP串匹配并行算法的MPI实现.docx
《KMP串匹配并行算法的MPI实现.docx》由会员分享,可在线阅读,更多相关《KMP串匹配并行算法的MPI实现.docx(14页珍藏版)》请在冰豆网上搜索。
KMP串匹配并行算法的MPI实现
KMP串匹配的MPI实现
一、简单匹配算法
KMP字符串模式匹配通俗点说就是一种在一个字符串中定位另一个串的高效算法。
简单匹配算法的时间复杂度为O(m*n);KMP匹配算法。
可以证明它的时间复杂度为O(m+n).。
先来看一个简单匹配算法的函数:
intIndex_BF(charS[],charT[],intpos)
{
/*若串S中从第pos(S的下标0≤posinti=pos,j=0;
while(S[i+j]!
='\0'&&T[j]!
='\0')
if(S[i+j]==T[j])
j++;//继续比较后一字符
else
{
i++;j=0;//重新开始新的一轮匹配}
if(T[j]=='\0')
returni;//匹配成功 返回下标
else
return-1;//串S中(第pos个字符起)不存在和串T相同的子串
}//Index_BF
此算法的思想是直截了当的:
将主串S中某个位置i起始的子串和模式串T相比较。
即从j=0起比较S[i+j]与T[j],若相等,则在主串S中存在以i为起始位置匹配成功的可能性,继续往后比较(j逐步增1),直至与T串中最后一个字符相等为止,否则改从S串的下一个字符起重新开始进行下一轮的"匹配",即将串T向后滑动一位,即i增1,而j退回至0,重新开始新一轮的匹配。
例如:
在串S=”abcabcabdabba”中查找T=”abcabd”(我们可以假设从下标0开始):
先是比较S[0]和T[0]是否相等,然后比较S[1]和T[1]是否相等…我们发现一直比较到S[5]和T[5]才不等。
如图:
当这样一个失配发生时,T下标必须回溯到开始,S下标回溯的长度与T相同,然后S下标增1,然后再次比较。
如图:
这次立刻发生了失配,T下标又回溯到开始,S下标增1,然后再次比较。
如图:
又一次发生了失配,所以T下标又回溯到开始,S下标增1,然后再次比较。
这次T中的所有字符都和S中相应的字符匹配了。
函数返回T在S中的起始下标3。
如图:
二、KMP算法
2.1KMP算法原理
给定两个串S=“S0S1......Sn-1”和T=“T0T1……Tm-1”,在主串S中寻找子串T的过程称为模式匹配,T称为模式串,若匹配成功,则返回模式串T在主串S中第一次出现的位置,若匹配失败,则返回一1。
KMP算法可以在O(m+n)的时间数量级完成串的模式匹配操作,其改进在于:
每当一趟匹配过程中出现字符比较不等时,就利用已经得到的“部分匹配”的结果将模式串“向右滑动”尽可能远的一段距离后,继续进行比较。
为了实现改进算法,需要解决下述问题:
当匹配过程中产生“失配”时,模式串“向右滑动”可行的距离有多远?
换句话说,当主串中第i个字符与模式串中第j个字符“失配”时,主串中第i个字符应与模式串中哪个字符再进行比较?
假设当“失配”时,模式串向右滑动的距离为k,则模式串中每个字符都对应一个k值,这个k值只依赖于模式串本身字符序列的构成,而与主串无关,用next[j]表示与Tj相对应的k值(O≤j≤m-1),则next定义如下:
Next[j]=-1 当j=0;
Next[j]=max{k} 当k-1Next[j]=0 其他
假设模式串T=“abaabaac”,根据公式计算next值。
j
0
1
2
3
4
5
6
7
T
a
b
a
a
b
a
a
c
Next
-1
0
0
1
1
2
3
4
KMP算法的基本思想:
在求得模式串的next值之后,匹配可按如下步骤进行,假设i和j分别指示主串和模式串中正待比较的字符,i和j的初值均为0。
若在匹配过程中Si=Tj,则i和j分别增1,继续进行比较;否则,i不变,而j退回到next[j]的位置继续比较,如此反复,直到出现下列两种可能:
(1)当j退回到某个next值时字符比较相等,则i和j分别增1,继续进行匹配;
(2)当j退回到—1,即模式串的第一个字符“失配”,则将主串向右滑动一个位置,使j=0,即从主串下一个字符Si+1起和模式串重新开始匹配。
例:
若主串S=“acabaabcabaabaacbc”,模式串T =“abaabaac”,则匹配过程如下:
第一趟:
主串:
acabaabcabaabaacbc(i=0,1)
模式串:
ab (j=0,1)
此时Next[1]=0,因而i不变'j=Next[j]=0;
第二趟:
主串:
acabaabcabaabaacbc(i=1)
模式串:
a (j=0)
此时next[0]=—1,因而i++,j=0
第三趟:
主串:
acabaabcabaabaacbc(i=2,3,4,5,6,7)
模式串:
abaaba (j=0,1,2,3,4,5)
此时Next[5]=2,因而i不变,j=Next[j]=2
第四趟:
主串:
acabaabcabaabaacbc(i=7)
模式串:
(ab)a (j=2)
此时Next[2]=O,因而i不变,j=Next[j]=0
第五趟:
主串:
acabaabcabaabaacbc(i=7)
模式串:
a(j=0)
此时next[0]=—1,因而i++,j=0
第六趟:
主串:
acabaabcabaabaacbc (i=8,9,10,11,12,13,14,15)
模式串:
abaabaac(j=0,1,2,3,4,5,6,7,8)
此时j=8,匹配成功。
从例子可以看出,在第三趟,模式串中第5个字符与主串字符比较失配时,主串指针i不变,继续与模式串中第2个字符比较,而T5=T2=’a’,匹配必然失败,显然此次比较是多余的,这说明KMP算法中给出的next的求值方法不是最优的,因此,对算法的优化改进就是对next的求值方法的改进。
2.2KMP算法实现
1、假设以i和j分别指示正文t和模式p中正等待比较的字符,令i、j的初值为0;若在匹配的过程中t[i]=p[j],则i与j分别加1;否则i不变,而j退到next[j]的位置继续比较(即j=next[j]),r若相等,则指针各自增加1;否则j再退到下一个next[j]值位置,依次类推,直到下列两种可能之一出现。
(1)j退到某个next值(next[...next[j]...])时,t[i]==p[j]成立,则i,j指针各自加1后继续比较。
(2)j退到-1(即模式的第一个字符“失配),此时需要将正文的指针i向右滑动一个位置,即从正文的下一个字符t[i+1]起和模式p重新从头开始比较。
voidgetnext(stringp,int*next)
{intlen=p.length();
intk=0;
next[k]=-1; //next[0]=-1
intj=-1;
while(k{ if(j==-1||p[j]==p[k])
{ k++; j++; next[k]=j; }
else
{ j=next[j]; } }
cout<<"next数组:
";
for(intx=0;x cout<2、求next[]:
next[0]=-1,j假设next[0],next[i],....next[i]的均已求出,现在要求next[i+1]的值。
由于在求解next[i]时已经得到p[i]之前最长真前缀和真后缀匹配的长度,设其值为j,即:
next[i]=j;如果此时进一步有p[j]==p[i],则p[i+1]之前最长真前缀的和真后缀匹配的长度就为j+1,且next[i+1]=j+1;反之,若p[j]!
=p[i],注意到,求p[i+1]之前的最长真前缀和真后缀匹配问题本质上仍然是一个模式匹配问题,只是在这里模式和正文都是同一个串p而已。
因此p[j]!
=p[i]时,应检查p[next[j]]与p[i]是否相等,若相等,则next[i+1]=next[j]+1,若仍然不想等,再取p[next[next[j]]]与p[i]进行比较,直到要将p[-1]与p[i]进行比较为止,此时next[i+1]=0。
voidgetnext(stringp,int*next)
{intlen=p.length();
intk=0;
next[k]=-1; //next[0]=-1
intj=-1;
while(k { if(j==-1||p[j]==p[k])
{ k++; j++; next[k]=j; }
else
{ j=next[j]; } }
cout<<"next数组:
";
for(intn=0;n
cout<2.3KMP算法与简单匹配算法比较
还是相同的例子,在S=”abcabcabdabba”中查找T=”abcabd”,如果使用KMP匹配算法,当第一次搜索到S[5]和T[5]不等后,S下标不是回溯到1,T下标也不是回溯到开始,而是根据T中T[5]==’d’的模式函数值,直接比较S[5]和T[2]是否相等,因为相等,S和T的下标同时增加;因为又相等,S和T的下标又同时增加。
最终在S中找到了T。
如图:
KMP匹配算法和简单匹配算法效率比较,一个极端的例子是:
在S=“AAAAAA…AAB“(100个A)中查找T=”AAAAAAAAAB”,简单匹配算法每次都是比较到T的结尾,发现字符不同,然后T的下标回溯到开始,S的下标也要回溯相同长度后增1,继续比较。
如果使用KMP匹配算法,就不必回溯.对于一般文稿中串的匹配,简单匹配算法的时间复杂度可降为O(m+n),因此在多数的实际应用场合下被应用。
KMP算法的核心思想是利用已经得到的部分匹配信息来进行后面的匹配过程。
看前面的例子。
为什么T[5]==’d’的模式函数值等于2(next[5]=2),其实这个2表示T[5]==’d’的前面有2个字符和开始的两个字符相同,且T[5]==’d’不等于开始的两个字符之后的第三个字符(T[2]=’c’).如图:
也就是说,如果开始的两个字符之后的第三个字符也为’d’,那么,尽管T[5]==’d’的前面有2个字符和开始的两个字符相同,T[5]==’d’的模式函数值也不为2,而是为0。
由上述可知,S[3]和T[0],S[4]和T[1]是根据next[5]=2间接比较相等,那S[1]和T[0],S[2]和T[0]之间之所以跳过,是因为S[0]=T[0],S[1]=T[1],S[2]=T[2],而T[0]!
=T[1],T[1]!
=T[2],==>S[0]!
=S[1],S[1]!
=S[2],所以S[1]!
=T[0],S[2]!
=T[0].还是从理论上间接比较了。
KMP算法时间复杂度的计算:
while循环使得执行次数出现了不确定因素。
我们将用到时间复杂度的摊还分析中的主要策略,简单地说就是通过观察某一个变量或函数值的变化来对零散的、杂乱的、不规则的执行次数进行累计。
KMP的时间复杂度分析可谓摊还分析的典型。
我们从上述程序的j值入手。
每一次执行while循环都会使j减小(但不能减成负的),而另外的改变j值的地方只有第五行。
每次执行了这一行,j都只能加1;因此,整个过程中j最多加了n个1。
于是,j最多只有n次减小的机会(j值减小的次数当然不能超过n,因为j永远是非负整数)。
这告诉我们,while循环总共最多执行了n次。
按照摊还分析的说法,平摊到每次for循环中后,一次for循环的复杂度为O
(1)。
整个过程显然是O(n)的。
这样的分析对于后面P数组预处理的过程同样有效,同样可以得到预处理过程的复杂度为O(m)。
其实,KMP的预处理本身就是一个T串“自我匹配”的过程
三、改进的KMP算法
3.1算法的基本思想
虽然模式匹配KMP算法的引入避免了算法中频繁的回溯,普遍提高了模式匹配的工作效率,但是经过分析,对KMP算法扫描部分还可做进一步的改进,下面介绍改进KMP算法的基本思想。
基本思想:
每当某趟匹配失败时,i指针不必回溯,而是利用己经得到的部分匹配结果,看看是否有必要将i的值进行调整,然后再将模式向右滑动∃若干位置后继续比较。
改进的KMP算法中的i不是保持不变,而是增加了,这就意味着加快了模式匹配的进度。
针对KMP算法的不足,下面给出新的next计算公式:
Next[j]=—1 当j=0;
Next[j]=—1 当k-1Next[j]=max{k} 当k-1=Tj
Next[j]=0其他
公式
(2)是主要对公式
(1)的第二种情况给出了改进,从而减少了多余的比较次数。
对模式串T=“abaabaac”,根据公式
(2)计算next值。
j
0
1
2
3
4
5
6
7
模式串
a
b
a
a
b
a
a
c
Next
-1
0
-1
1
-1
-1
-1
4
根据改进的next值,上例的匹配过程如下:
第一趟:
主串:
aeabaabcabaabaaebc(i=0,1)
模式串:
ab (j=0,1)
此时next[1]=0,因而i不变,j=next[j]=0;
第二趟:
主串:
acabaabcabaabaaebc(i=1)
模式串:
a (j=0)
此时next[0]=一l,因而i++,j=0
第三趟:
主串:
acabaabcabaabaaebc(i=2,3,4,5,6,7)
馘串:
abaaba (j=0,1,2,3,4,5)
此时next[5]=一1,因而i++j=0
第四趟:
主串:
acabaabcabaabaaebe (i=8,9,10,11,12,13,14,15)
模式串:
abaabaac(j=0,1,2,3,4,5,6,7,8) 此时j=8,匹配成功。
3.2KMP改进算法的实现
voidgetnext(char*t,int*next)
{inti,j;
inttlen=strlen(t);
i=0;j=-1;
next[0]=-1;
while(i if(j=-1||t[i]==t[j])
{
i++;j++;
if(t[i]==t[j])
next[i]=-1;
else
next[i]=j;
}
else
j=next[j];
}
intkmp_match(char*s,char*t,intpos)
{
inti,j,slen,tlen;
int*next=(int*)malloc(sizeof(int));
slen=strlen(s);
tlen=strlen(t);
*next=tlen;
getnext(t,next);
i=pos;
j=0;
while(i if(j==-1||s[i]==t[j])
{i++;j++;}
elsej=next[j];
if(j free(next);
returni-tlen;}
3.3算法的分析
KMP算法时间复杂度可分两部分来进行分析,一部分是扫描算法中T[i]与P[j]的比较次数,另一部分是根据模式来计算next[j]的工作量。
计算next[j]与模P的长度m有关,与主串无关,其算法时间复杂度为O(m)。
因为KMP算法与改进的KMP算法涉及的计算next[j]的工作量相同,所以这两个算法的时间复杂度主要区别在于,假设p(m,n)与q(m,n)分别为KMP算法和改进KMP算法中T[i]与P[j]的平均比较次数,则考虑到:
if语句的两个分支出现的概率相等,平均出现次数为n/2;else分支语句取得n/2个next[j]后,i不得增加却又投入了比较,使得在同一个i位置上增加了一次比较,因此:
p(m,n)=n+n/2+m=1.5n+m。
在改进的KMP算法中的平均出现次数为n/2,该分支语句使得i向右滑动的机会增加了n/4次,这些机会不是使i保持不变,而是使i朝着匹配处前进了亦即促进了模式匹配的进度,因此:
q(m,n)=1.5n-n/4+m=1.25n+m.m为恒定值,故而limn%&q(m,n)p(m,n)=limn%&1.25n+m1.5n+m=0.833可见,p(m,n)与q(m,n)同阶,它们的时间复杂度都为O(m+n),但q(m,n)的系数要比p(m,n)的低,因此改进后的KMP算法更能提高字符串模式匹配的工作效率。
四、MPI并行算法
4.1并行算法原理
算法思路:
将长为n的文本串T均匀划分成互不重叠的p段,分布于处理器0到p-1中,且使得相邻的文本段分布在相邻的处理器中,显然每个处理器中局部文本段的长度为「n/p」(最后一个处理器可在其段尾补上其它特殊字符使其长度与其它相同)。
再将长为m的模式串P和模式串的newnext函数播送到各处理器中。
各处理器使用改进的KMP算法并行地对局部文本段进行匹配,找到所有段内匹配位置。
但是每个局部段(第p-1段除外)段尾m-1字符中的匹配位置必须跨段才能找到。
一个简单易行的办法就是每个处理器(处理器p-1除外)将本局部段的段尾m-1个字符传送给下一处理器,下一处理器接收到前一处理器传来的字符串后,再接合本段的段首m-1个字符构成一长为2(m-1)的段间字符串,对此字符串做匹配,就能找到所有段间匹配位置。
但是算法的通信量很大,采用下述两种改进通信的方法可以大大地降低通信复杂度:
1)降低播送模式串和newnext函数的通信复杂度。
利用串的周期性质,先对模式串P做预处理,获得其最小周期长度、最小周期个数及后缀长度,只需播送和部分函数就可以了,从而大大减少了播送模式串和函数的通信量。
2)降低每个处理器,将本局部文本段的段尾m-1个字符中找到P的最长前缀串,因为每个处理器上都有模式串信息,所以只需传送该最长前缀串的长度就行了。
这样就把通信量从传送模式串P的最长前缀匹配串降低到传送一个整数,从而大大降低了通信复杂度。
而且KMP算法结束时模式串指针j-1的值就是文本串尾模式串最大前缀串的长度,这就可以在不增加时间复杂度的情况下找到此最大前缀串的长度。
4.2并行算法实现
intmain(intargc,char*argv[])
{ intsize,myid;
inttag=1;
T串定义;
P串定义;
intn,m,plen,tlen,*next;
MPI_Status status;
MPI_Init(&argc,&argv);
MPI_Comm_size(MPI_COMM_WORLD,&size);
MPI_Comm_rank(MPI_COMM_WORLD,&myid);
plen=strlen(P);
tlen=strlen(T);
m=plen-1;
n=(tlen+size-1)/size;
next=(int*)malloc((plen+1)*(sizeof(int)));
getnext(P,next);
if(myid==0)
kmp_match(T,P,next,0,n,myid);
elseif(myid==size-1)
kmp_match(T,P,next,(myid*n-m>0)?
(myid*n-m):
0,tlen,myid);
else
kmp_match(T,P,next,(myid*n-m>0)?
(myid*n-m):
0,(myid+1)*n,myid);
MPI_Finalize();
free(next);
return0;
}
4.3运行结果显示
在192.168.150.217上:
编译:
运行:
首先运行gen_ped生成模式串:
之后可以使用命令mpirun–npSIZEkmpmn来运行该串匹配程序:
存储在pattern.dat中的模式串为:
nwn
存储在match_result中的匹配结果为:
结论
本文针对模式匹配算法中KMP模式匹配算法在匹配过程中存在的一些不足进行了改进,给出了一种改进的KMP改进算法的设计思想、原理,并对其进行了详细的