模式匹配算法

算法一:朴素的模式匹配算法

假设我们要从主串s="goodgoogle"找到t="google"这个子串的位置,我们需要下列步骤

1、主串s的第1位开始,s与t前三个字符都匹配成功,第四个字符不匹配(竖线表示相等,闪电状弯折表示不想等)

2、主串s的第2位开始,匹配失败

3、主串s的第3位开始,匹配失败

4、主串s的第4位开始,匹配失败

5、主串s的第5位开始,s与t,6个字符全部匹配成功

对主串的每一个字符作子串开头,要与匹配的字符串进行匹配。对主串作大循环,每一个字符开头作t长度的小循环,直到匹配成功或者全部遍历完为止。


#include<iostream>
using namespace std;
//返回子串t在主串t中第pos个字符之后的位置;若不存在,函数返回0
//t非空,1≤pos≤s.size()
int Index(string s, string t, int pos=0)        //默认为0,默认从第一个元素开始查找
{
	int i = pos;				//主串t当前下表
	int j = 0;				//子串t当前下表
	while (i<s.size() && j<t.size())	//i小于s长度,j小于t长度,循环继续
	{
		if (s[i] == t[j])
		{
			i++;
			j++;
		}
		else				//指针退回,重新开始匹配
		{
			i = i - j + 1;	        //i退回到上一次匹配首位的下一位
			j = 0;			//j退回到子串t的首位
		}
	}
	if (j >= t.size())
		return i - t.size() + 1;	//i - t.size()表示从s[4]开始重复,+1表示该元素为第5个元素
	else
		return 0;
}

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

复杂度:

最好的情况,比如在"googlegood"中找"google",时间复杂度为O(1)

稍差一点,比如"abcdefgoogle"中找"google",时间复杂度O(n+m),其中n为主串长度,m为子串长度,根据等概率原则,平均(n+m)次查找,时间复杂度为O(n+m)

最坏的情况,每次不成功都发生在串t的最后一个字符,比如子串t="0000000001",9个"0"和一个"1"

主串s="00000000000000000000000000000000000000000000000001",49个"0"和一个"1"

时间复杂度为O((n-m+1)*m)



算法二:KMP模式匹配算法

假设主串s="abcdefgab",t="abcdex"

"abcdex"的首字符"a"与后面的串"bcdex"中任何一个字符都不相等,子串t与主串s的前5个字符分别相等,意味着子串t的首字符"a"不可能与s串的第2位到第5位的字符相等


上面的案例,子串t中没有重复的字符,下面给出一个有重复字符的例子

假设s="abcababca",t="abcabx"

第一步,前5个字符相等,第6个字符不相等,根据前面的经验,t的首字符"a"与t中第二位、第三位字符军部等,不需要判断

子串t中第1位与第4位相等,第2位与第5位相等;主串s中第4位,第5位分别与子串t中第4位,第5位相等,意味着子串的第1位与第2位分别与主串第4位与第5位相等,不需要判断


总结:i值不回溯,j值的变化只与子串有关,取决于t串中的结构中是否有重复的字符串。

我们把t串各个位置的变化定义为一个数组next,next的长度就是t串的长度


举例说明next数组:


#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 pos = 1)	        //默认从主串的第一个位置开始查找
{
	int i = pos;					//i用于表示主串s的当前位置
	int j = 1;					//j用于表示子串t的当前位置
	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])		//两字符串相等,或j处于开始位,继续
		{
			i++;
			j++;
		}
		else					//j值退回合适的位置,i值不变
			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;
}

时间复杂度:对于get_next来说,t的长度为m,由于只涉及到简单循环,其时间复杂度为O(m);i的值不回溯,主串的长度为n,while循环的时间复杂度为O(n),因此整个代码的数减复杂度为O(n+m)


算法三:KMP模式匹配算法改进

KMP匹配算法还是有缺陷的,比如子串s="aaaabcde",子串t="aaaaax",子串t的next为012345


我们发现②③④⑤步骤是多余的。由于子串t中第2、3、4、5个位置上的字符串和守卫上的字符串相等,那么可以用首位next[1]来代替后续与它相等的字符后续的next[j]


取代的数组为nextval[],代码如下

#include<iostream>
using namespace std;
//求模式串t的next函数修正值并存入数组nextval
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])		//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回溯
	}
}

int Index_KMP(string s, string t, int pos = 1)
{
	int i = pos;
	int j = 1;
	int next[255];
	get_nextval(t, next);
	while (i < s.size() && j < t.size())
	{
		if (j == 0 || s[i] == t[j])
		{
			i++;
			j++;
		}
		else
			j = next[j];
	}
	if (j >= t.size())
		return i - t.size() + 1;
	else
		return 0;
}

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

猜你喜欢

转载自blog.csdn.net/happyjacob/article/details/80850975
今日推荐