字符有一类重要运算:模式匹配。设T和P是两个字符串(T的长度为n,P的长度为m,1<=m<=n),T 为目标,P为模式,在T中查找是否有与P相等的子串。如果有,则给出P在T的匹配位置,这个运算被称为模式匹配,模式匹配的方法主要有两种:
(1)朴素的模式匹配,也称Brute Force算法
(2)KMP算法
(3)BM算法
一、朴素的模式匹配
算法思想:从目标串的的第一个字符起与模式串的第一个字符比较,若相等,则继续对字符进行后续的比较,否则目标串从第二个字符起与模式串的第一个字符重新比较,直至模式串中的每个字符依次和目标串中的一个连续的字符序列相等为止,此时称为匹配成功,否则匹配失败。最坏的情况是每遍比较都在最后出现不等,即没变最多比较m次,最多比较n-m+1遍,总的比较次数最多为m(n-m+1),因此朴素的模式匹配算法的时间复杂度为O(mn),效率低下。
for(i=0;i<na;i++)
{
k=i;
j=0;
while(a[k]==b[j])
{
k++;j++;
}
if(j==nb)
break;
}
printf("%d\n",k-j);
二、KMP算法
Brute Force算法存在大量的重复运算,为了避免重复运算,引入KMP算法。复杂度O(m+n)
设P为“ABCABB”,这个串中“AB”是P的前缀,也是串“ABCAB”的后缀。所以如果在匹配的时候直到“ABCAB”都匹配成功,而“ABCABB”匹配失败,所以T肯定有串为“ABCAB”,所以可以直接将j指针指向移动到第k位。k满足:
P[0 ~ k-1] == P[j-k ~ j-1]
A | B | C | A | B | C(i) | D | H | I | J |
A | B | C | A | B | B(j) |
|
A | B | C | A | B | C(i) | D | H | I | J |
A | B | C(j) | A | B | B |
利用已经部分匹配这个有效信息,保持i指针不回溯(T),通过修改j指针(P),让模式串尽量地移动到有效的位置。
suffix[0]=-1; //设置P前缀的边界值
suffix[1]=0;
int k=0; //前缀函数指针初始化
for(int i=2 ; i<=m ; i++)
{
while(k>=0 && p[k]!=p[i-1]) //沿前缀函数指针追溯p中与p[i-1]相同的字符
k=suffix[k]; //位置k,即目标串当前字符与p[i]匹配成功失败时,应与p[k+1]比较
suffix[i] = ++k;
}
有了前缀函数suffix[],可将P匹配T的过程写成一重循环,从P[0]出发,依次T[0],T[1],T[2],...,T[n-1]。
若P[j]和T[i]匹配成功,则下一次匹配P[j+1]和T[i+1].
若P[j]和T[i]匹配失败,则T[i]不断的匹配P[suffix[j]],P[P[suffix[j]]],...,直到匹配成功(T[i]==P[..P[suffix[j]]])或者匹配失败(P[...p[suffix[j]]]=-1);若匹配成功,则下一次比较P[P[...P[suffix[j]]]+1] 与 T[i+1] ;若匹配失败,则下一次比较P[0]与T[i+1]
i=0;
j=0; //T和P的匹配指针初始化
while(i<n-1 && j<=m-1)
{
if(j==-1 || t[i]==p[j]) //若沿前缀函数指针匹配T[i]失败,则下一次比较p[0]与T[i+1];
i++;j++; //若匹配成功,则下一次比较P[j+1]与T[i+1];
else
j=suffix[j]; //取suffix
}
if(j>m-1) //匹配完毕
return (i-(m-1));
else
return (-1); //匹配失败