字符串模式匹配

模式匹配又称为子串的定位操作,是串最基本的操作。下面对常用的模式匹配算法作简单介绍。

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算法。
2、KMP算法
KMP算法是D.E.Knuth、V.R.Pratt和J.H.Morris同时发现的,因此将该算法称为克努特—莫里斯—普拉特算法,简称为KMP算法。朴素算法的时间复杂度之所以较高,是因为存在回溯,而KMP算法的改进之处正是对模式串进行预处理,在匹配过程中消除这种回溯,提高匹配效率。因此KMP算法的关键之处在于对模式串的预处理,而对模式串的处理与主串是无关的。
我们先来看下面的例子。假如,A="abababaababacb",B="ababacb",我们来看看KMP是怎么工作的。我们用两个指针i和j分别表示,A[i-j+ 1..i]与B[1..j]完全相等。也就是说,i是不断增加的,随着i的增加j相应地变化,且j满足以A[i]结尾的长度为j的字符串正好匹配B串的前 j个字符(j当然越大越好),现在需要检验A[i+1]和B[j+1]的关系。当A[i+1]=B[j+1]时,i和j各加一;什么时候j=m了,我们就说B是A的子串(B串已经整完了),并且可以根据这时的i值算出匹配的位置。当A[i+1]<>B[j+1],KMP的策略是调整j的位置(减小j值)使得A[i-j+1..i]与B[1..j]保持匹配且新的B[j+1]恰好与A[i+1]匹配(从而使得i和j能继续增加)。我们看一看当 i=j=5时的情况。

   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)。相对朴素匹配算法来说,在时间复杂度方面有很大的提高。

对串匹配的研究除了当前提到的两种方法以外,还有其他诸如后缀树等方法,这些方法也都是利用了预处理,使得线性时间内可解决字符串匹配问题。


 


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值