模式匹配又称为子串的定位操作,是串最基本的操作。下面对常用的模式匹配算法作简单介绍。
1、朴素模式匹配算法
此算法是最简单的匹配算法,算法思想如下:从主串s的第1个字符起和模式串t的第1个字符比较,如果相等,则继续比较后继字符。否则从主串的下一个字符起再重新和模式串的第1个字符进行比较,直到模式串t中的每一个字符依次和主串s中的一个字符序列相等则匹配成功,返回和模式串t中第1个字符相等的字符在主串s中的位置;否则匹配不成功,返回-1。例如主串s为:ababcabcacbab,模式串t为:abcacb,匹配过程如下:
第1趟:ababcabcacbab i=3
abc j=3 匹配失败
第2趟:ababcabcacbab i=2
a j=1 匹配失败
第3趟:ababcabcacbab i=7
abcac j=5 匹配失败
第4趟:ababcabcacbab i=4
a j=1 匹配失败
第5趟:ababcabcacbab i=5
a j=1 匹配失败
第6趟:ababcabcacbab i=11
abcacb j=6 匹配成功
如上所示,当第6趟匹配的时候在s中找到t,此时匹配成功,返回与t第1个字符相等的字符在s中出现的位置。算法实现如下:
//匹配算法:主串s,模式t
int index(char *s, char *t)
{
int i = 0, j = 0;
while(i < strlen(s) && j < strlen(t))
{
if(s[i] == t[j])
{
i++; j++;
}
else //每一趟匹配失败时重新计算主串s和模式串t的索引
{
i = i - j + 1;
j = 0;
}
}
if(j >= strlen(t)) //匹配成功
return i - j + 1;
else //匹配失败
return -1;
}
由上述分析我们可以得出该算法的时间复杂度为O(mn)(其中n为主串长度,m为模式串长度)。当主串和模式串都比较长时,实现匹配所需的时间可能也是我们所不能接受的,虽然现在的硬件已经很先进,但是对此算法仍需要进行改进,下面介绍一种对此算法的改进——KMP算法。
i = 1 2 3 4 5 6 7 8 9 ……
A = a b a b a b a a b a b …
B = a b a b a c b
j = 1 2 3 4 5 6 7
此时,A[6]<>B[6]。这表明,此时j不能等于5了,我们要把j改成比它小的值j'。j'可能是多少呢?仔细想一下,我们发现,j'必须要使得B[1..j]中的头j'个字母和末j'个字母完全相等(这样j变成了j'后才能继续保持i和j的性质)。这个j'当然要越大越好。在这里,B [1..5]="ababa",头3个字母和末3个字母都是"aba"。而当新的j为3时,A[6]恰好和B[4]相等。于是,i变成了6,而j则变成了4:
i = 1 2 3 4 5 6 7 8 9 ……
A = a b a b a b a a b a b …
B = a b a b a c b
j = 1 2 3 4 5 6 7
从上面的这个例子,我们可以看到,新的j可以取多少与i无关,只与B串有关。我们完全可以预处理出这样一个数组next[j],表示当匹配到B数组的第j个字母而第j+1个字母不能匹配了时,新的j最大是多少。next[j]应该是所有满足B[1..next[j]]=B[j-next[j]+1..j]的最大值。然后,A[7]=B[5],i和j又各增加1。这时,又出现了A[i+1]<>B[j+1]的情况:
i = 1 2 3 4 5 6 7 8 9 ……
A = a b a b a b a a b a b …
B = a b a b a c b
j = 1 2 3 4 5 6 7
由于next[5]=3,因此新的j=3:
i = 1 2 3 4 5 6 7 8 9 ……
A = a b a b a b a a b a b …
B = a b a b a c b
j = 1 2 3 4 5 6 7
这时,新的j=3仍然不能满足A[i+1]=B[j+1],此时我们再次减小j值,将j再次更新为next[3]:
i = 1 2 3 4 5 6 7 8 9 ……
A = a b a b a b a a b a b …
B = a b a b a c b
j = 1 2 3 4 5 6 7
现在,i还是7,j已经变成1了。而此时A[8]居然仍然不等于B[j+1]。这样,j必须减小到P[1],即0:
i = 1 2 3 4 5 6 7 8 9 ……
A = a b a b a b a a b a b …
B = a b a b a c b
j = 0 1 2 3 4 5 6 7
终于,A[8]=B[1],i变为8,j为1。事实上,有可能j到了0仍然不能满足A[i+1]=B[j+1](比如A[8]="d"时)。因此,准确的说法是,当j=0了时,我们增加i值但忽略j直到出现A[i]=B[1]为止。求数组next的代码如下:
int next[100];
void getnext(char *b)
{
int i = 1, j = 0;
next[1] = 0;
while(i <= strlen(b))
{
if(j == 0 || b[i - 1] == b[j - 1])
{
i++;
j++;
next[i] = j;
}
else
{
j = next[j];
}
}
}
计算出next数组以后,根据上面的描述我们可以轻松写出KMP算法的具体实现:
int kmp(char *a,char *b)
{
int i = 1, j = 1; //i是主串中的位子 ,j匹配串的位子
while(i <= strlen(a) && j <= strlen(b))
{
if(j == 0 || a[i - 1] == b[j - 1])
{
i++;
j++;
}
else
j = next[j];
}
if(j > strlen(b))
return i - strlen(b);
else
return 0;
}
KMP具体实现过程和求next的数组的过程是相似的。利用KMP算法实现的模式匹配的时间复杂度为O(n)。相对朴素匹配算法来说,在时间复杂度方面有很大的提高。