字符串模式匹配算法
什么是模式匹配?
模式匹配就是子串在主串种的定位运算。也叫串匹配。
假设我们有俩个字符串:T(目标串)和P(模式串);在目标串T种查找模式串P的定位过程。称为模式匹配,
模式匹配有俩种结果:
目标串种找到模式为T的子串,返回P在T中的起始位置下标值。
未成功匹配,返回-1
BF算法
BF算法为朴素匹配算法也叫暴力算法,效率较低。
算法基本思想:
- 将目标串T第一个字符串与模式串P的第一个字符比较;
- 若相等,则比较T和P的第二个字符
- 若不相等,则比较T的下一个字符P的第一个字符
- 重复以上步骤,直到匹配成功或者目标串T结束
代码如下:
#include <stdio.h>
#include <stdlib.h>
#define maxlen 255
typedef struct
{
char str[maxlen];
int length;
}bf;
int BF(bf T,bf P)
{
int j=0,i=0,ret=-1;
while((j<strlen(P.str))&&(i<strlen(T.str)))
{
if(P.str[j]==T.str[i])//字符串相等则继续
{
i++;
j++;//目标串和字符串进行下一个字符的匹配
}
else{
i=i-j+1;
j=0;
}
}
if(j=strlen(P.str))
{
ret=i-j+1;
}
return ret;
}
int main()
{
bf P,T;
printf("-------BF算法----");
printf("输入主串:\n");
scanf("%s",T.str);
printf("输入子串:\n");
scanf("%s",P.str);
printf("%d\n",BF(T,P));
printf("Hello world!\n");
return 0;
}
BF算法效率分析:
最坏时间复杂度O(mn)
空间复杂度O(1)
KMP算法
快速模式匹配算法,简称KMP算法,是在BF算法基础上改进得到的算法。学习BF算法我们知道,该算法的实现过程就是傻瓜式用模式串与主串中的字符一一匹配,算法执行效率不高。KMP算法不同,它的实现过程接近人为进行模式匹配。如果匹配相等的前缀序列种又某个后缀正好是模式的前缀,那么就可以将模式向后滑动到与这些相等字符对齐的位置,主串i指针无需回溯,并从该位置继续比较。而模式向后滑动位数的计算仅与模式本身的结构有关,与主串无关。不论主串如何变换,只要给定模式串,则匹配失败后移动的距离就已经确定了。
KMP算法的核心思想:部分匹配,即不再把主串的位置移动到已经比较过的位置(不再回溯),而是根据上一次的比较结果继续后移。
KMP算法的核心点:不要回溯到无效的地方,让其回溯到有效的位置。
回溯的位置的特点:就是最长公共前后缀重合的地方,也就是说让最长相同前缀对齐后缀。
通俗讲,每次匹配失败后模式串移动的距离不一定是1,某些情况下一次可移动多个位置。
不仅如此,模式串种任何一个字符都有可能导致匹配失败,因此串中每个字符都应该对应一个数字,用来表示匹配失败后模式串移动的距离。
模式串向后移动等价于指针j向前移,也就是模式串后移相当于对指针j重定位。
因此,我们可以给每个模式串配备一个数组(例如next[]),用于存储模式串中每个字符对应指针j重定向的位置(也就是存储模式串的数组下标),比如j=3,则该字符匹配失败后指针j指向模式串中第3个字符 。
模式串中各个字符对应next值的计算方式是,取该字符前面的字符串(不包含自己),其前缀字符串和后缀字符串相同字符的最大个数再+1就是该字符对应的next值。
前缀字符串指的是位于模式串起始位置的字符串,例如模式串“ABCD”,则“A”,“AB”,“ABC”以及“ABCD”都属于前缀字符串; 后缀字符串指的是位于串结尾处的字符串,例如模式串"ABCD"来说,“D”“CD”“BCD”和“ABCD”为后缀字符串。
模式串中第一个字符对应的值为0,第二个字符对应1,这是固定不变的。
从图中可以看出,当字符E匹配失败时,指针j指向模式串数组中第2个字符即B。
为什么字符C对应的next值为1?
因为字符AB前缀字符串和后缀字符串相等的个数为0,0+1=1。
那么,为什么字符E的next值为2?
因为E的next紧挨着该字符之前的A与模式串开头字符A相等,1+1=2。
如果模式串为ABCABE,则对应next数组应为[0,1,1,1,2,3]
为什么字符E的next值为3?
因为紧挨着该字符前面的AB与开头的AB相等,2+1=3。
注:
在实际KMP算法中,为了使公式更简洁,计算简单,如果串的位序是从1开始的,则next数组才需要整体加1;如果串的位序是从0开始的,则next数组不需要整体加1。
算法设计过程:
刚开始令j指向模式串中第1个字符,i指向第2个字符。接下来,每个字符操作如下:
如果i和j指向的字符相等,则i后面的第一个字符的next值为j+1,同时i和j做自加1操作,为求下一个字符的next做准备
上图中可以看到,字符a的next值为j+1=2,同时i和j都做了加一操作。当计算字符C的next值时,判断i和j指向的字符是否是相等,显然相等,因此令该字符串的next值为j+1=3,同时i和j自加1(此次next值的计算使用了上一次j的值)
如图,计算字符d的next时,i和j指向的字符不相等,这表明最长的前缀字符串”aaa”和后缀字符串“aac”不相等,接下来要判断次长的前缀字符串“aa”和后缀字符串“ac”是否相等,这一步的实现可以用j=next[j]来实现。
从上图可以看出,i和j指向的字符又不相同,因此继续做j=next[j]的操作。
可以看到,j=0表明字符d前的前缀字符串和后缀字符串相同个数为0,因此如果字符d导致了模式匹配失败,则模式串移动的距离只能是1。
next数组的C语言代码:
void Next(char *T,int *next){
next[1] = 0;//例子:aaacd
next[2] = 1;
int i = 2;
int j = 1;
while(i<strlen(T)){
if(j==0||T[i-1]==T[j-1]){
//i=2,j=1 T[1]=T[0] a=a ;
//i=3,j=2 T[2]=T[1] a=a ;
//j=0
i++;//3,4,5
j++;//2,3,1
next[i]=j; //next[3] = 2;next[4]=3,next[5]=1
}
else{
//i=4,j=3,T[3]!=T[2] c!=a
//i=4,j=2,T[3]!=T[1] c!=a
//i=4,j=1,T[3]!=T[0] c!=a
j=next[j];
//j=next[3]=2;
//j=next[2]=1;
//j=next[1]=0;
}
}
}
代码中的j=next[j]的运用可以这样理解,每个字符对应的next值都可以表示该字符前同后缀字符串相同的前缀字符串最后一个字符所在位置,因此再每次匹配失败后,都可以轻松找到次长前缀字符串的最后一个字符与该字符进行比较。
Next函数的缺陷
在图a中,当匹配失败时,Next函数会由图b开始进行模式匹配,但是从图中可以看到,这样做时没有必要的,纯浪费时间。
出现这种多余操作,问题在于T[i-1] = T[j-1]成立时,没有继续对i++和j++后的T[i-1]和j[j-1]的做判断。
优化后的Next函数
改进后的Next函数如下所示:
void get_Next(char *T,char *next)
{ //例子:abababaaababaa
next[1]=0;
next[2]=1;
int i=2;
int j=1;
while(i<strlen(T)){
if(j=0||T[i-1]==T[j-1]){
//T[2]=a,T[0]=a
//T[3]=b,T[1]=b
i++;//3,4,5
j++;//1,2,3
if(T[i-1]!=T[j-1]){
next[i]=j;
}
else{
//T[2]=a T[0]=a
//T[3]=b,T[1]=b
//T[4]=a,T[2]=a
next[i]=next[j];
//next[3]=next[1]=0
//next[4]=next[2]=1
//next[5]=next[3]=0
}
}
else{
j=next[j];
//j=next[1]=0
}
}
}
KMP算法的实现
假设主串A为“ababcabcacbab”,模式串B为“abcac”,则KMP算法执行过程:
第一次匹配如图所示,匹配结果失败,指针j移动至next[j]的位置
第二次匹配如图所示,匹配结果失败,依旧执行j=next[j]操作
使用KMP算法只需匹配3次,而同样的问题使用BF算法则需匹配6次才能完成。
KMP算法的完整C语言实现代码为:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void Next(char* T ,int *next)
{
int i =1;
int j =0;
next[1]=0;
while(i<strlen(T)){
if(j==0||T[i-1]==T[j-1])
{
i++;
j++;
next[i] = j;
}
else{
j=next[j];
}
}
}
int KMP(char *S,char *T){
int next[10];
Next(T,next);//根据模式串初始化next数组
int i=1;
int j=1;
while(i<=strlen(S)&&j<=strlen(T)){
//j==0:代表模式串的第一个字符就和当测试的字符不相等;
//S[i-1]==T[j-1],如果对应位置字符相等,俩种情况下,
//指向当前测试的俩个指针下标i和j都向后移
if(j==0 || S[i-1]==T[j-1]){
i++;
j++;
}
else{
j=next[j];//如果测试的俩个字符不相等,
//i不动,j变为当前测试字符串的next值
}
}
if(j>strlen(T)){//如果条件为真则匹配成功
return i-(int)strlen(T);
}
return -1;
}
int main()
{
int i=KMP("ababcabcacbab","abcac");
printf("%d",i);
return 0;
}
KMP效率
KMP效率分析:
最坏时间复杂度O(m+n),其中,求next数组时间复杂度O(m),模式匹配过程最坏时间复杂度O(n)
尽管普通模式匹配的时间复杂度是O(mn),KMP算法的时间复杂度是O(m+n),但在一般情况下,普通模式匹配的实际执行时间近似为O(m+n),因此至今仍被采用。KMP算法仅在主串与子串有很多”部分匹配“时才显得比普通算法要快的多,其主要优点是主串不回溯。
参考:http://data.biancheng.net/view/180.html