串的模式匹配算法:暴搜 / KMP 详解

串的模式匹配

串的模式匹配与子串查找(子串定位)的概念相同,即在主串s中查找子串t(称为模式)第一次出现的起始位置索引。

例如:

  1. s=“ABCDEF”,t=“CD”,若字符位置序号从0开始,则t在s中的匹配位置号。
  2. s=“aabaaaa”,t=“aa”,则t在s中共有4次匹配,匹配位置号分别是0、3、4、5。

匹配成功:子串包含于主串中
匹配失败:子串不包含于主串中

子串定位基本算法(暴力搜索)

直接先丢代码(C++)

int index(string s, string t){
	int i=0,j=0;
	// 遍历完s或t
	while(i<s.length() && j<t.length()){
		// 如果匹配,就继续往后进行判断
		if(s[i]==t[j]){
			i++; j++;
		}
		// 如果不匹配,进行回溯
		else{
			i=i-j+1;
			j=0;
		}
	}
	if(j==t.length()) return i-j;	// 匹配成功,返回索引
	return -1;		// 匹配不成功,返回-1
}

这个思路其实很简单,就是串t串s中一个一个地进行匹配:
在这里插入图片描述
如上图,ts在匹配到第j个字符的时候,匹配失败,这个时候,i就要回溯到x+1位置,重新匹配字符串(请搭配代码一同服用)。
这个算法的缺点就是时间复杂度会达到O(m*n),并且显得有点傻: 比如,现在S串为‘AAAAAAAAAAAAAAAA’,t串为‘AAAAAAB’。暴力搜索就会显得效率不高,每次‘A’和‘B’不匹配,t串就要回到第一个位置上的‘A’与S重新匹配。

改进的模式匹配算法:KMP

改进点:子串匹配失败时,主串下标i不需要回溯 KMP算法的时间复杂度为O(m+n),其中m n分别为主串、子串的长度。
算法发现者:D. E. Knuth, J. H. Morris和V. R. Pratt同时发现,故命名为KMP算法,即克努特-莫里斯-普拉特算法。

1.算法思想

当子串j位置与主串i位置匹配失败时,能否让主串下标i不动,将子串k号位置(k<j)与主串i号位置对应比较大小?

要实现这一想法,需要满足两个条件:

  1. t[0, .., k-1] = s[i-k, ..., i-1] 子串t[k]之前k个字符与主串s[i]之前k个字符相等,接下来需要判断s[i]和t[k]是否相等。
  2. 主串小于i-k号位置不可能有成功匹配 这个条件主要是避免错过了第一次索引的位置,为了满足这个条件,只需要在1<=k<j范围内取得满足1.条件的最大k。

合并1.2.两个条件:t[0,…,k-1] = s[i-k,…,i-1] (1<=k<j), 其中k满足以下条件:
不存在k’, 使1<=k’<j且k’>k且t[0…k’-1]=s[i-k’…i-1]。
条件(1)(2)也相当于 max{k|1<=k<j 且t[0..k-1]=s[i-k..i-1]} 条件(3)


说明
这(1)(2)两个条件是什么意思呢,其实很简单,我举个例子,比如现在S串为ABBABBABBABC,T串为ABBABBABC
当匹配到:
S: ABBABBABBABC (i=8)
T: ABBABBABC (j=8)
现在s[i]和t[j]不匹配了,我们希望i不动,找到一个k值来让t[k]和s[i]继续匹配,首先这个k值我们希望是1<=k<j,并且t[0,…,k-1] = s[i-k, …, i-1](条件1)。
满足条件1的K值有两个一个是k=2,一个是k=5。

  1. 当k=2时
    S:ABBABBABBABC
    T:XXXXXXABBABBABC
    最终匹配失败
  2. 当k=5时
    S:ABBABBABBABC
    T:XXXABBABBABC
    最终匹配成功

所以我们要满足(1)的基础上满足k是最大值的情况(满足条件(2))。


当找到这个k值之后,当s[i]和t[j]匹配失败时,可将模式串t向右滑动,使得t[k]与s[i]对齐,然后从t[k]与s[i]开始匹配字符
如果找不到k值,那说明主串i号位置之前都不可能有成功的匹配,则令j=0,开始新的一轮匹配。


A. 为什么当s[i]与t[j]失配时,若存在下标k满足条件 (3),为什么主串中下标i-k之前不可能有子串的成功匹配?
证明
这个用反证法就能证明,假设存在在i-j+1~i-k-1内存在下标x是子串的成功匹配,则s[x, …, i-1]=t[0, …, i-x-1],因为i-j+1<=x<=i-k-1,有k+1<=i-x<=j-1,即存在k’=i-x满足条件(3),而k’>k,这与条件2相矛盾。

B. 对子串任意下标位置 j,怎么求满足条件(3)的k值?
E. Knuth等人发现,这个问题的求解与主串无关(独立于主串),它只与子串自身有关,可以证明,条件(3)与以下条件(4)等价:
max{k|1<=k<j 且t[0..k-1]=t[j-k..j-1]}
证明

  1. 先证明(3)成立时,(4)成立。 因为条件(3)成立,有t[0…k-1]=s[i-k…i-1], 当s[i]与t[j]失配(s[i]!=t[j])时,有s[i-j…i-1]=t[0…j-1],因为k<j,故i-k>i-j,必有s[i-k…i-1]=t[j-k…j-1],所以,t[0…k-1]=t[j-k…j-1],即条件(4)成立。
  2. 在证明(4)成立时,(3)成立。 因为条件(4)成立,有t[0…k-1]=t[j-k…j-1], 当s[i]与t[j]失配(s[i]!=t[j])时,有s[i-j…i-1]=t[0…j-1], 因为k<j,故i-k>i-j,必有s[i-k, i-1]=t[j-k, j-1],所以,t[0…k-1]=s[i-k, i-1],即条件(4)成立。

2.next数组

根据上一部分,我们已经知道了为什么KMP可以这么处理(不移动主串i,只将子串回溯到k位置),以及需要满足的条件(max{k| 1<=k<j 且 t[0,…,k-1] = t[j-k,…,j-1]})。在实际处理时,我们不用每次都计算一次k值,而是使用一个数组next[n]用来存储满足条件的k值,n为子串t的长度。
如果某个位置j不存在满足条件的k值,那么next[j]=0,表示要回溯到串t的首部,再继续与主串的i位置匹配。如果j=0时,子串与主串i位置还是匹配失败,就使得i+=1。所以可令next[0]=@,@为特殊标志值(一般取-1)。所以,我们得到完整的next数组值的定义如下:
在这里插入图片描述

3.next数组值的手工求法

方法:对子串任意j位置(j=0, 1, 2, …, n-1) n为子串长度,在1<=k<j范围内观察子串j号位置之前是否有k个字符与子串开头的k个字符相同(k由大到小试探)。
举例:
在这里插入图片描述
当你能够正确理解这两个例子,说明你前面的东西理解了。

4.计算next数组值的程序算法

基本思路: 递推法
1)设next[0]=-1;
2)设next[0,…,j-1]已经求得,以下分析求next[j]的方法。
在这里插入图片描述
其中,k0是满足条件t[0,…,k0-1]=t[j-k0-1,…,j-2]的最大整数。不失一般性,设kp(p=1, 2, 3, …)是满足以下条件的最大整数:kp-1>kp且t[0,…,kp-1]=t[j-kp-1,…,j-2]。
可见,序列{k0, k1, k2, …}是一个递减的下标序列,且最后两个下标必为0,-1。设kq-1=0,kq=-1,则k0, k1, …, kq-2均大于0,即k0>k1>k2>… >kq-2>kq-1=0>kq=-1。

next[j]的求法如下:
在这里插入图片描述

说明
在这里插入图片描述
比如这个例子:

  1. next[0]=-1;
  2. j=1,next[1]:k0=next[0]=-1(kq),不存在p值,所以next[1]=kq+1=0;
  3. j=2,求next[2]: k0=next[1]=0,t[k0]!=t[j-1];k1=next[k0]=-1,不存在p值,所以next[2]=k1+1=0;
  4. j=3,next[3]:k0=next[2]=0,t[k0]==t[j-1],所以next[3]=k0+1=1;
  5. j=4,next[4]:k0=next[3]=1,t[k0]!=t[j-1];k1=next[k0]=0, t[k1]==t[j-1],next[4]=k1+1=1;
  6. …依次类推

直接看程序:

// t不为null
void get_next(string t, vector<int> & next){
	next.push_back(-1);
	for(int j=1; j<t.length(); j++){
		int k=next[j-1];
		while(k!=-1 && t[j-1]!=t[k]){
			k = next[k];
		}
		next.push_back(k+1);
	}
}

测试结果:
在这里插入图片描述
正式的求next数组程序,只有一个单层循环:

void get_next(string t, vector<int> & next){
	int j=1,k=-1;
	next.push_back(-1);
	while(j<t.length()){
		if(k==-1 || t[j-1]==t[k]){
			next.push_back(k++);
			j++;
		}
		else k = next[k];
	}
}

其实两个程序的时间复杂度是一样的,但是这么写感觉清爽些?哈哈哈


4. KMP模式匹配算法
int index_kmp(string s, string t, vector<int>& next[]){
	int i=j=0;
	while(i<s.length() && j<t.length()){
		if(j==-1 || s[i]==t[j]){
			i++; j++;
		}
		else j=next[j];
	}
	if(j==t.length()) return i-j;
	return -1;     // 匹配失败 返回-1
}

5. nextval数组

其实关于next数组还有一种改进,叫做nextval,这一部分等有时间再更新吧。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
KMP算法(Knuth-Morris-Pratt算法)是一种用于解决字符匹配问题的高效算法。它的主要思想是利用匹配失败时的信息,尽量减少比较次数,提高匹配效率。 KMP算法的核心是构建一个部分匹配表(Partial Match Table),也称为Next数组。这个表记录了在匹配失败时应该将模式向右移动的位置。 构建部分匹配表的过程如下: 1. 首先,将模式中的第一个字符的Next值设为0,表示当匹配失败时,模式不需要移动; 2. 然后,从模式的第二个字符开始,依次计算Next值; 3. 当第i个字符与前面某个字符相同的时候,Next[i]的值为该字符之前(不包括该字符)的相同前缀和后缀的最大长度; 4. 如果不存在相同的前缀和后缀,则Next[i]的值为0。 有了部分匹配表之后,KMP算法匹配过程如下: 1. 用i和j来分别表示模式和主的当前位置; 2. 如果模式中的字符和主中的字符相同,那么i和j都向右移动一位; 3. 如果模式中的字符和主中的字符不同,那么根据部分匹配表来确定模式的下一个位置; 4. 假设当前模式的位置为i,根据部分匹配表中的值Next[i],将模式向右移动Next[i]个位置; 5. 重复上述步骤,直到找到匹配或者主遍历完毕。 KMP算法的时间复杂度为O(m + n),其中m和n分别是模式和主的长度。相比于匹配算法的时间复杂度为O(m * n),KMP算法能够大幅减少比较次数,提高匹配效率。 综上所述,KMP模式匹配算法通过构建部分匹配表并利用匹配失败时的信息,实现了高效的字符匹配。在实际应用中,KMP算法被广泛地应用于文本编辑、数据索和字符处理等领域。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值