字符串匹配

字符串匹配一直是比较重要的一个要点,可以有很多种算法实现,最好的当然就是比较有名的KMP算法了,下面把算法导论上的所有算法都实现一遍。

一:朴素字符串匹配算法

就是最简单的思路,两个递归遍历一遍,复杂度是O((n-m+1)*m)

二:Rabin—Karp算法

基本思路是将字符串的每个字符转化为对应的数字,比如“5651”就对应数5651,这样只要每次算出数,进行数的比较即可。它的最坏复杂度也是O((n-m+1)*m),但是平均情况要好很多。

/*如果出现很大的数可以通过mod一个素数来实现,
**我这里只是进行了简单的实现*/
#define __DEBUG 0
#if __DEBUG
	#define DEBUG(fmt,...) printf(fmt,__VA_ARGS__)
#else
	#define DEBUG(fmt,...)
#endif

#include<iostream>
#include<cstring>
#include<cmath>
#include<algorithm>	
using namespace std;
const int maxn = 5000;
char t[maxn],p[maxn];
int s[maxn];
int d, h, pt,q;
int n, m;

int quick_pow(int a, int b)
{
	int sum = 1;
	while (b)
	{
		if (b % 2 == 1) sum = sum*a;
		a =a*a;
		b >>= 1;
	}
	return sum;
}

int main()
{
	q = 13, d = 10;
	scanf("%s%s", t,p);
	n = strlen(t);
	m = strlen(p);
	int h = quick_pow(d, m - 1);
	DEBUG("%d\n", h);
	pt = 0;
	s[0] = 0;
	for (int i = 0; i < m; i++)
	{
		pt = (d*pt + p[i] - '0');
		s[0] = (d*s[0] + t[i] - '0');
	}
	DEBUG("%d %d\n", pt, s[0]);
	for (int i = 0; i <= n - m; i++)
	{
		if (pt == s[i])
			printf("Pattern occurs with shift %d\n",i);
		else if (i < n - m)
			s[i + 1] = d*(s[i] - (t[i] - '0')*h) + t[i + m] - '0';
	}
	return 0;
}

三:有限自动机实现字符串匹配

使用有限自动机,先将所有的状态转移写成状态转移图,然后进行处理即可。算法导论上写的比较难理解。时间复杂度的话匹配部分是O(n),预处理部分是O(m^3|∑|),可以改进为O(m|∑|),所以总和为O(m|∑|)+O(n)。
基本思路参照这个博客:点击打开链接
我自己理解的时候主要是这个状态转移图中的回溯边想不清楚,搞了很长时间才明白:状态转移图与T无关,只要考虑P,因为当前P一定是当前T的后缀,所以假如现在P为ab,新加入字符a如果匹配,就沿着主线+1即可;如果不匹配,将a加到ab后形成aba,寻找它最长的前缀使其与aba后缀相等,这里就是a,这样就返回状态1,也就是现在T的新后缀是aba,那么原本的ab就不能用了,重新从a开始,也就是状态1。确实比较难理解。

#include<iostream>
#include<cstring>
#include<algorithm>
#include<map>
#include<vector>
using namespace std;

bool MatchPerfix(const char *P, int k, int q, char a)
{
	if (k == 0)
		return true;
	if (k == 1)
		return P[k - 1] == a;
	return P[k - 1] == a&&strncmp(P, P + q - k + 1, k - 1)==0;//找出最长的P中前缀==后缀的子串
}
vector<map<char, int>> ComputeTransionFunction(const char *P, const char * alpha)//产生状态转移图
{
	int n = strlen(P);
	int j = 0;
	int k = 0;
	vector<map<char, int>> transion_map(n + 1);
	for (int q = 0; q <=n; q++)//代表P的第q的字符进行比较
	{
		j = 0;
		while (alpha[j] != '\0')
		{
			k = min(n, q + 1);//k作为状态
			while (!MatchPerfix(P, k, q, alpha[j]))
			{
				k--;
			}
			transion_map[q][alpha[j]] = k;
			j++;
		}
	}
	return transion_map;
}
void PrintTransionMap(vector<map<char, int>> transion_map)//输出状态转移图
{
	typedef vector<map<char, int>>::iterator VCI;
	typedef map<char, int>::iterator MCI;
	for (VCI iter = transion_map.begin(); iter != transion_map.end(); iter++)
	{
		map<char, int> m = *iter;
		for (MCI p = m.begin(); p != m.end(); p++)
		{
			cout << p->first << "--" << p->second << "  ";
		}
		cout << endl;
	}
}
void FiniteAutomatonMatcher(const char *P, const char *T, const char *alpha)
{
	int n = strlen(P);
	int m = strlen(T);
	int q = 0;
	vector<map<char, int>> transion_map = ComputeTransionFunction(P, alpha);
	//PrintTransionMap(transion_map);
	for (int i = 0; i < m; i++)
	{
		q = transion_map[q][T[i]];
		if (q == n)
		{
			cout << "The pattern occures with shift " << i - n + 1 << endl;
		}
	}
}

int main()
{
	const int max_lengh = 1000;
	const char *alpha = "abcdefghijklmnopqrstuvwxyz";
	char P[max_lengh];
	char T[max_lengh];
	while (gets_s(T))
	{
		gets_s(P);
		FiniteAutomatonMatcher(P, T, alpha);
		cout << "Enter next case\n";
	}
	return 0;
}

四:KMP算法

kmp算法是字符串匹配的最佳算法了,时间复杂度是O(n),真是强无敌。基本思路跟有限自动机一样,都是先找出状态转移图,也就是每个长度对应的最长真前缀,比有限自动机好的地方在于,有限自动机需要把字符串每个长度对应的所有出现过的字符的状态转移存储,而KMP只要简单的进行P中状态转移即可,简单很多。

#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int maxn = 1500;


void ComputePrefixFunction(char P[],int next[])
{
	int m = strlen(P);
	memset(next, 0, sizeof(next));
	next[0] = 0;
	int k = 0;					//k作为前缀最长长度
	for (int q = 1; q < m; q++)
	{
		while (k > 0 && P[k] != P[q])
			k = next[k-1];//如果新的一个字符不能与+1后的k匹配,就要找当前前缀的前缀
		if (P[k] == P[q])
			k++;
		next[q] = k;
	}
}
bool KMPMatcher(char T[], char P[], int next[])
{
	int n = strlen(T);
	int m = strlen(P);
	ComputePrefixFunction(P, next);
	int q = 0;
	for (int i = 0; i < n; i++)
	{
		while (q > 0 && P[q] != T[i])//最新的一个字符不能匹配,寻找当前匹配子串的最长真前缀
			q = next[q - 1];
		if (P[q] == T[i])
			q++;
		if (q == m)
		{
			cout << "Pattern occurs with shift : " << i - m + 1 << endl;
			return true;
		}
	}
	return false;
}

int main()
{
	int next[maxn];
	char P[maxn], T[maxn];
	while (cin>>T)
	{
		cin >> P;
		if (!KMPMatcher(T, P, next))
			cout << "There is no catch" << endl;
		cout << "Enter next case" << endl;
	}
	return 0;
}





posted @ 2016-10-23 14:00  seasonal  阅读(120)  评论(0编辑  收藏  举报