一篇文章带你通关BF算法与KMP算法

对于两个字符串,在主串中s中查找是否存在某一个子串t(一个串中任意个连续字符组成的子序列(包含空串)称为该串的子串),如果存在则返回串t在串s中第一次出现的位置。串s为主串,子串t成为模式串,这个过程也称为模式匹配*,下面来详细介绍下串的两种模式匹配算法:BF算法KMP算法

1.BF算法

BF算法也称暴力匹配算法或有回溯的匹配算法,具体步骤为:

  • 从主串s的第一个字符开始与t的第一个字符做比较
  • 如果相等,继续逐个比较后续的字符
  • 如果不相等,使主串的第二个字符与t的第一个字符比较
  • 如果相等,继续逐个比较后续的字符
  • 如果不相等,使主串的第三个字符与t的第一个字符比较
  • 不断循环上面步骤,直到匹配成功或匹配失败(即到主串末尾结束)

以一个例子来进行讲解:

(1) 首先串s的第一个字符’a’与串t的第一个字符’c’相比较
在这里插入图片描述

(2)两者不相等,则模式t向后滑动,使串s的第二个字符与串t的第一个字符比较
在这里插入图片描述

(3)两者不相等,模式t向后滑动,使串s的第三个字符与串t的第一个字符进行比较
在这里插入图片描述

(4)两者相等,继续逐个比较后续字符(就不再画图了)
(5)匹配成功,返回模式串t第一次在串s中出现的位置

设主串长度为m,模式串t长度为n,当主串前m-n个字符都是在比较到串t的最后一个字符匹配不成功时,模式串每个字符都比较了m-n次,总共的比较次数为(m-n)n,最后主串剩余的n个字符与模式串t的比较次数最多为n^2次,所以总的比较次数为最多为mn次,BF算法的复杂度为O(mn)

2.BF算法代码实现

int BF(char* s, char* t) {
	if (s == NULL || t == NULL) {
		PRSN;	//打印串指针为空,为我们定义的一个宏
		return NEQUAL;		//NEQUAL表示不相等,为我们定义的一个宏
	}
	int i = 0, j = 0;
	while (s[i] != '\0' && t[j] != '\0') {
		if (s[i] == t[j]) {//两者相等,继续对比后续字符
			i++;
			j++;
		}
		else {
			//主串从下一个位置开始
			//因为j每次从0开始,因此j的值代表匹配过的长度
			//所以此次比较主串开始字符的角标为i-j
			//下次匹配时从下一个字符开始,所以+1
			i = i - j + 1;
			//模式串t从头开始
			j = 0;
		}
	}
	if (t[j] == '\0') {//如果j的值等于模式t的长度,则说明t已经完全匹配
		return i - j;
	}
	else {
		return NEQUAL;	
	}
}

3.KMP算法

KMP算法也称无回溯的模式匹配,在BF算法中,我们可以看到由于主串的i指针不停的回溯,导致算法的复杂度较高,当字符串较大时,将会耗费比较长的时间。于是KMP算法使主串的i指针不回溯,利用已经匹配的部分结果,使模式串尽可能向右滑动,减少匹配次数。

如在下图中,s[5]!=t[5],如果按照BF算法,i需要回溯到1,j需要回溯到0,然后再重新进行逐个比较,但是由匹配结果我们知道在j=5之前都有s[i]=t[j],又因为t[0]=t[3],t[1]=t[4],所以t[0]=s[3],t[1]=s[4],所以下次我们直接比较s[5]与t[2]即可,无需将i回溯,只需将j回溯到2即可
在这里插入图片描述

简单来说,KMP算法就是当匹配到某处不成功时,保持i不变,将j回溯到某个位置,因此我们修改一下BF的代码就得到了KMP的代码框架:

4.KMP算法代码框架

int KMP(char* s, char* t) {
	if (s == NULL || t == NULL) {
		PRSN;
		return NEQUAL;
	}
	int i = 0, j = 0;
	while (s[i] != '\0' && t[j] != '\0') {
		if (s[i] == t[j]) {//两者相等,继续对比后续字符
			i++;
			j++;
		}
		else {
			//i不变,让j改变
		}
	}
	if (t[j] == '\0') {//如果j的值等于模式t的长度,则说明t已经完全匹配
		return i - j;
	}
	else {
		return NEQUAL;	//NEQUAL表示不相等,为我们定义的一个宏
	}
}

那么每次j该如何回溯呢?

5.next数组

在上面我们谈到,KMP利用之前匹配的结果,具体而言,如果对串t的某个字符t[j] (0≤j≤n-1),若存在一个整数k(1≤k<j),使得模式串t中k所指字符的前k个字符t[0],…,t[k-1]和t[j]前面的k个字符t[j-k],…,t[j-1]相同,并与主串s中i所指字符之前的k个字符相同,那么j下次可以直接回溯到k,i无需回溯

T[j-k] ~ T[j-1] = S[i-k] ~ S[i-1]
T[0] ~ T[j-1] = S[i-k] ~ S[i-1]
T[0] ~T[k-1] = T[j-k] ~ T[j-1]

K取何值时效率才最高呢?显然在满足上诉条件的前提下,K越大越好,即:

max{1≤k<j且T[0] ~T[k-1] = T[j-k] ~ T[j-1]}

用next数组来存储串t中每个位置j对应的k,next[j]=k,表示当S[i]!=T[j]时,j指针的下一个位置,next数组求法如下所示:

  1. next[0]=-1,因为j已经在最左边了,如果这时候不匹配,只有将i向后移动

  2. next[1]=0,这个很好理解,第二个位置匹配错误时,直接让j回退到0即可

  3. j>1时,若t[k]=t[j]时,next[j+1]=k+1
    为什么呢?因为T[0] ~ T[k-1] = T[j-k] ~ T[j-1],
    如果T[k]=T[j],
    那必然T[0] ~ T[k] = T[j-k] ~ T[j],
    由图也可以看出,当绿色的部分相等时,如果T[k]=T[j],那蓝色的部分也必 然相等(即使两个绿色的部分有重合也是如此),
    所以next[j+1]=k+1
    在这里插入图片描述

  4. j>1时,若t[k] != t[j]时,k=next[k]
    当t[k] != t[j]时,显然上图中蓝色部分不再相等,这时我们无法求next[j+1],需要把k回退,k回退之前的位置为next[k],将k回退到next[k],之后再将t[k]与t[j]比较,如此往复直到t[k]==t[j]或者k回退到了-1,这时给next[j+1]赋值0即可,或者next[j+1]=k+1,这也是开始next[0]=-1而非0的好处(这其实是一种递归思想,读者可以好好体会下)

6.next数组获取的代码实现

void GetNext(char* t, int* next) {
	if (t == NULL || next == NULL) {
		printf("指针为空\n");
		return;
	}
	next[0] = -1;
	int len = strlen(t);
	if (len == 1) { 
		return; 
	}
	int j = 1;
	int k = 0;
	next[1] = 0;
	while (j < len) {
		if ((k == -1) || (t[k]==t[j])) {
			next[++j] = ++k;	//(1)
		}
		else {
			k = next[k];	//(2)
		}
	}
}

可以看到代码并不复杂,但(1),(2)两句的逻辑需要读者仔细琢磨

7.KMP代码实现

有了next数组,我们再返回到上面的KMP代码,想要获取j的改变值通过next数组即可

int KMP(char* s, char* t, int next[]) {
	if (s == NULL || t == NULL) {
		PRSN;
		return NEQUAL;
	}
	int i = 0;
	int j = 0;
	while (s[i] != '\0' && t[j] != '\0') {
		if (j==-1 || s[i] == t[j]) {//两者相等,继续对比后续字符
			i++;
			j++;
		}
		else {
			//i不变,让j改变
			j = next[j];
		}
	}
	if (t[j] == '\0') {//如果j的值等于模式t的长度,则说明t已经完全匹配
		return i - j;
	}
	else {
		return NEQUAL;	//NEQUAL表示不相等,为我们定义的一个宏
	}
}

8.KMP算法复杂度

设主串s长度为m,模式串t长度为n,获取next数组时由循环结构可知算法复杂度为O(n),在主串与模式串匹配过程中,因为i不回溯,所以复杂度为O(m),所以KMP算法复杂度为O(m+n),m远大于n时为O(m),因此当m,n均较大KMP算法效率是远高于BF算法的

完整代码:
链接:https://pan.baidu.com/s/15ZGdrDzaHZY9em6HecKI0Q
提取码:zfpp

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值