今日学习总结--模式匹配算法

模式匹配算法

所谓模式匹配就是在一个主串s中找到子串t的位置,对于复杂的字符串,那么使用怎样的方式去查找就显得尤其重要了。

1、朴素的模式匹配算法
为什么称为朴素呢?因为简单易理解,大家都可以想到,实现起来也相对简单,主要完全可以实现功能。

思路:主串s和子串t都从第一位开始比较,如果相等则比较第二位…如果其中某一位不相等了,则将s回退到i-j+1位置(每次比较一旦遇到不相同,就从开始位置后移一位),j回退到0.(子串又一次从头开始比较)

如下述例子
图1:当比较到第4位发现不相等后,i = i - j +1—> 1
而j = 0; 右开始了新一轮的比较
图2:比较主串第2位的时候,发现不相等,i = i - j +1—> 2
…经过一系列的失败匹配后,移动到了图5
图5:主串从第5开始匹配子串,然后发现全部匹配成功,结束。


以下为实现代码:

#include<iostream>

using namespace std;
//返回子串t在主串t中第pos个字符之后的位置;若不存在,函数返回0
//t非空,1≤pos≤s.size()

int Index(string s, string t){
	int i = 0;
	int j = 0;//两个串都是要从头开始比较
	while (i < s.size() && j < t.size()){
		//一旦有一个串等于了size()要么就是t到头 说明找到了   如果是s到头 说明没有找到
		if (s[i] == t[j]){
			i++;
			j++;
		}
		else{
			i = i - j + 1;//主串位置从开始比较位置后移一位
			j = 0;
		}
	}
	if (j >= t.size()){
		return i - t.size() + 1; //只有子串到头了 才说明找到了
	}
	else{
		return 0;
	}
}

int main()
{
	string s = "goodgoogle";
	string t = "google";
	cout << Index(s, t) << endl;
	return 0;
}

分析一下时间复杂度:

(最坏)具体如
主串是s=“00000000000000000000000000000000000000000000000001”
子串是 t=“00001” 则在比较的过程中,每次都要判断到第五位才能发现不相等 然后又在主串后移一位之后继续比较5次…如此重复,则事件复杂度是O((n-m+1)*m)

基于人类如果在比较的时候,发现前面那些位都是相同的只有某一位不同,就不会在比较前面的,而是直接从不同的那一位开始比较。
因此出现了主串位置不回溯,那么子串应该如何选择最恰当的位置以减少比较次数呢?

2、KMP模式匹配算法

首先假设是这种情况:

因此发现不相同之后,就直接从不相同的位置以及子串第一位开始再次比较。

上面的例子具有特殊性,因为也有可能出现很多子串后面是有相同部分的。

这样,是不是就省去了第4 第5 两个位置的重复比较。

因此,KMP的关键在于比较之前预先遍历子串形成一个next数组

next数组作用:在于当子串和主串出现不相等之后,可以参考next数组,知道j将要回溯到的位置是多少。

如何构建next数组?
首先,第一位的next值为0,第二位的next值为1。联系next数组的功能很容易想到为什么要如此设计前两位的值;
其次,求解每一位的next值时,根据前一位进行比较,使用前一位和他指向的next数组值对应下标的值进行比较,如果相等,那么+1就是当前所求位的next值;
如果出现不等的情况,那就用前一位的next值指向的位置的值的next值指向的位置的值进行比较,如果相等,那么当前所求的next值就是前前位值的next值+1;
如果一直没有找到,当到头之后,所求位值的next就是1.

具体看例子:
例如有一个子串(也叫做模式串) t = "a b a a b c a c " 相应的next值应该是 0 1 1 2 2 3 1 2



有了以上分析相信应该掌握了求解next数组的思想,那么通过下述例子练习一下看看:

由此可以实现KMP算法代码

KMP代码

#include<iostream>
using namespace std;

//通过计算,返回子串的next数组
void get_next(string t, int *next)
{
	int i, j;
	i = 1;
	j = 0;
	next[1] = 0;
	while (i<t.size() - 1)
	{
		if (j == 0 || t[i] == t[j])		//t[i]表示后缀的单个字符,t[j]表示前缀的单个字符
		{
			i++;
			j++;
			next[i] = j;
		}
		else
			j = next[j];			//若字符不相同,则j值回溯
	}

}

//返回子串t在主串t中第pos个字符之后的位置;若不存在,函数返回0
//s[0]和t[0]应该保存字符串的长度,这里我们随意保存一个字符#

int Index_KMP(string s, string t)	
{
	int i = 1;					
	int j = 1;					
	int next[255];					//定义一个next数组
	get_next(t, next);				//对串做分析,得到next数组

	while (i < s.size() && j < t.size())		//若i小于s的长度且j小于t的长度,循环继续
	{
		if (j == 0 || s[i] == t[j])		
		{
			i++;
			j++;
		}
		else					//当发现主串和子串不相等后,主串i位置不回溯,子串通过next数组得到一个合适的位置
			j = next[j];
	}
	if (j >= t.size())
		return i - t.size() + 1;
	else
		return 0;
}

int main()
{
	string s = "#goodgoogle";
	string t = "#google";
	cout << Index_KMP(s, t) << endl;
	return 0;
}

针对以上KMP的一点改进

分析例子:
主串 s = “aaaabcde” 子串 t = “aaaaax” 如果我们使用之前的方法,求出next是 012345 然后我们就会发现执行的过程是

比较时候,当你发现第5位不相等之后,查找next数组拿到j = next[5] = 4, 然后就出现了第2步,接着依然不相等…就有了2 3 4 5 这几步无效的操作。
实际上发现2 3 4 5这几个位置上的值是和第1个位置上的值相等之后,完全可以用第一个位置的next值替代后面的next值,也就是说,不相等之后,直接将j回退到第一位开始比较。

改进后的KMP算法

void get_nextval(string t, int *nextval)
{
	int i, j;
	i = 1;
	j = 0;
	nextval[1] = 0;
	while (i<t.size() - 1)
	{
		if (j == 0 || t[i] == t[j])		
		{
			i++;
			j++;
			if (t[i] == t[j])		//若当前字符与前缀字符相等
			{
				nextval[i] = nextval[j];//将前缀字符的nextval值赋值给nextval在i位置上的值
			}
			else
				nextval[i] = j;		//当前的j为nextval在i位置上的值
		}
		else
			j = nextval[j];			//不相等,j回溯
	}
}

仅仅调整一下 get_nextval函数即可

到此,实现了KMP。分析时间复杂度为 得到next数组的n和后面匹配时候的m,则O(m+n)

更多可关注 sdrzzx2019

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值