字符串匹配算法_KMP

昨天初步学习了下字符串匹配的算法,有朴素匹配法(又称Brute-Force)、Rabin-Karp算法、有限自动机匹配法 以及 KMP算法。

本文只是列出KMP算法的代码,以及我个人的一点儿肤浅的理解。我是做好准备,将来理解得更深入时,会更新本文。

本文代码又是基本照搬别人的>_<。。原文在此 http://billhoo.blog.51cto.com/2337751/411486


约定:模式串Pattern,记为P,其长度为m;目标串Test,记为T,其长度为n;字符的集合记为Σ

隐含假设:模式串一般比目标串短,故m = O(n)


四种算法对比分析:

朴素匹配法BF

朴素匹配法的之所以效率低,说到底就是信息冗余了,没有有效利用。而后三个都是一定程度上减小了信息冗余度,提高了效率。

后三个算法,在匹配过程中,分为两步操作,第一步对字符串进行预处理,第二步再进行匹配。其中预处理是减少冗余信息的关键步骤。

Rabin-Karp

Rabin-Karp在匹配时利用子串的hash值的比较,达到匹配的目的。这本身并没有带来冗余度的减少,但是因为计算某一个子串A的hash值之后,能够根据它的值,在很短时间内计算出下一个子串B的hash值,从而提高匹配效率。这里的信息冗余就是子串A和子串B重叠那一部分子串的hash值,减少了不必要的第二遍计算。当然,我们要考虑到模式串Pattern可能比较长,如果把它看做|Σ|进制的一个数,可能会太大不方便存储。于是,改进的版本,就利用数论知识,通过每一步计算都对某个常数q取余,降低数的数量级。这样一来,不会影响到本来就匹配的所有命中点,但是会增加一些伪命中点。当遇到一个可能的命中点时,通过最朴素的方法——逐位的字符匹配(运行时间O(m)),来进一步验证到底是命中点还是伪命中点。

去伪办法还是逐位匹配,而最坏的情况是每次都伪命中,每次都要逐位匹配,所以Rabin-Karp算法的最坏运行时间还是O(m*n)。期望运行时间比较难算,近似认为伪命中的概率等于从1到q的q个数中选中q的概率,即为1/q,如此 则期望运行时间为O(n) + O(m(v + n/q))。其中q是选取的一个常数(一般为素数),v是有效位移数。如果选取q >= m且v = O(1) ,则期望运行时间为O(n)。

有限自动机匹配法FAM 

FAM方法,借助有限自动机,为字符串的所有可能匹配过程建立状态和转移。其中包含5个要素:Q, q0, A, Σ 和 δ 。

Q为状态的有限集合,q0为初始状态, A是接受状态的集合, Σ 是字符的集合(也称输入字母表), δ 是一个从Q * Σ到Q的函数,成为有限自动机的转移函数。

δ (i, a) = j 即表示状态i接受字符a就转入状态j。

预处理阶段,FAM方法可以在O(m^3 * |Σ|)时间内(改进版本可以做到O(m * |Σ|)的时间),计算出全部的状态转移函数。然后在匹配阶段,利用状态转移函数,在O(n)时间内进行匹配。改进版本的总时间为 O(m * |Σ|) + O(n)。

Knuth-Morris-Pratt算法KMP

KMP算法跟FAM方法有类似的思想,只不过它在预处理阶段可以用O(m)的时间计算出前缀函数PI,处理时间是O(n),故总时间O(m) + O(n) = O(n)。

KMP比FAM高效一点儿,后者因为要对每一个字符a∈Σ都进行计算,故预处理时间多了一个Σ因子。

KMP算法代码

计算前缀函数PI

void computePrefixFunc(char *P, char *PI, int sizeP, int sizePI)
{
	int q = 2;//已处理的字符数

	/*PI数组大小为sizePI == sizeP + 1,
	其中PI[0]无意义不使用, PI[1] == 0是很自然的事*/
	PI[1] = 0;

	int k = 0;//当前前缀长度


	for(;q <= sizeP;q++)
	{
		/*如果当前不匹配,递归地赋值为
		其前缀位置*/
		while(k > 0 && P[k] != P[q - 1])
		{
			k = PI[k];
		}

		/*如果匹配,前缀长度加一*/
		if(P[k] == P[q-1])
		{
			k++;
		}

		/*记录q位置的当前前缀值*/
		PI[q] = k;

	}

}


KMP匹配

void KMP_match(char *P, char *PI, char *T, int sizeP, int sizePI, int sizeT)
{
	computePrefixFunc(P, PI, sizeP, sizePI);

	int i = 0;
	int q = 0;//当前匹配成功的长度

	for(;i < sizeT;i++)
	{
		/*如果当前匹配不成功,递归地查看其前缀位置,
		直到匹配成功或者q == 0*/
		while(q > 0 && P[q] != T[i])
		{
			q = PI[q];
		}

		/*如果匹配,则匹配长度加一*/
		if(T[i] == P[q])
		{
			q++;
		}

		/*如果匹配长度达到模式串的长度,则找到了一个成功的匹配,
		此时将匹配长度置为其前缀长度,做好准备接受下一个匹配*/
		if(q == sizeP)
		{
			std::cout<<"String matched with shift "<<i - sizeP + 1<<std::endl;
			q = PI[q];
		}
	}

}

我的测试用例:

#include <iostream>
#include <cstdlib>
#include <ctime>

#define P_SIZE 5
#define T_SIZE 300000
#define MAX_FOR_RAND 10

int main(int argc, char* argv[])
{
	char pArr[P_SIZE];
	char tArr[T_SIZE];
	char PI[P_SIZE];
	srand((unsigned int)time(NULL));//生成伪随机数种子

	/*随机生成模式串和目标串*/
	for(int i = 0;i < P_SIZE;i++)
	{
		pArr[i] = 'a' + rand() % MAX_FOR_RAND;
	}

	for(int i = 0;i < T_SIZE;i++)
	{
		tArr[i] = 'a' + rand() % MAX_FOR_RAND;
	}

	/*计算KMP_match运行的时间*/
	clock_t start_time = clock();

	KMP_match(pArr, PI, tArr, P_SIZE, P_SIZE + 1, T_SIZE);

	clock_t end_time = clock();

	double cost = (double)(end_time - start_time) / CLOCKS_PER_SEC * 1000;

	std::cout<<"Running time is "<<cost<<" ms."<<std::endl;

	return 0;
}</ctime></cstdlib></iostream>


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值