KMP算法原理

本文详细介绍了KMP算法的基本原理,与暴力搜索算法相比,KMP算法通过避免回溯显著提高了字符串搜索效率。文章进一步阐述了KMP算法的核心思想、Next数组的计算方法以及完整实现代码,为读者提供了一套高效解决字符串匹配问题的解决方案。

1. 摘要

这篇博客系统地介绍了模式匹配(KMP)算法的基本原理。在字符串搜索过程中,传统的暴力搜索方法在失配时,必须回溯重新搜索。回溯产生了时间复杂度高的问题,降低了字符串搜索的效率。针对暴力搜索这一缺点,Donald Knuth、Vaughan Pratt、James H. Morris三人于1977年提出了KMP算法。KMP算法在搜索发生失配时,将模式串向右滑动到某个位置重新开始匹配而不是回溯,提高了搜索效率。经过分析,KMP算法的时间复杂度为O(m+n),暴力搜索算法的时间复杂度为O(mn),KMP算法的搜索效率远远高于暴力搜索算法。

2. 暴力搜索算法原理

暴力搜索算法从文本串和模式串首部逐个地进行匹配,当发生失配时,文本串回溯到文本串本次比较开始处的下一个位置,而模式串回溯到首位置。

暴力搜索算法步骤:

说明:假设要在文本串text中查找模式串pattern。文本存储在text的0~n-1的单元中,模式存储在pattern的0~m-1中。

输入: 串text和pattern

输出:如果在text中找到pattern,则返回pattern在text中的起始位置,否则,返回-1

(1)初始化index, i和j为0;index是text和pattern比较的起始位置,i扫描text,j扫描pattern;

(2)当i<n且j<m且n-i>=m时,

若text[i] =pattern[j],则i和j都增1;

否则,i 回溯到 i-j+1, j回溯到0,并且index增1。

(3)如果j不等于m,则index等于-1;

(4)返回index。

暴力搜索算法代码:

int violent_kmp(char* text, char* pattern)
{
	int i=0;
	int j=0;
	int index=0;
	int n = strlen(text);
	int m = strlen(pattern);

	while(i<n && j<m && n-i>=m)
	{
		if(text[i] == pattern[j])
		{
			i++;
			j++;
		}
		else
		{
			i = i - j + 1;
			j = 0;
			index += 1;
		}
	}

		if(j != m)
			index = -1 ;
	
	return index;

}

3. KMP算法原理

考虑两个普通的文本串text和pattern。text串用{t0, t1, ..., tn}表示,pattern串用{p0, p1, ..., pm}表示。则text[i]=ti,pattern[j]=pj。在text中搜索pattern过程中,进行到这样一个状态:pattern[j]前j个字符和text[i]前i个字符一一匹配,但是pattern[j]与text[i]比较时失配。如下图1所示。

                                                                                                 


kmp图1

                                             图1 模式串pattern在位置 j 失配的状态


针对暴力搜索算法的缺点,要避免回溯,必须将pattern向右移动,让text[i]和pattern[k]开始匹配,这里k<j。图2 展示了pattern串在位置j处失配后,向右滑动在位置k处恢复搜索的状态。

kmp图2

                                                                                                                                             图2 pattern串向右滑动后匹配状态


从图2 明显可以看出,为了让text[i]和pattern[k]将搜索继续下去,pattern前的k个字符必须和pattern[k]之前的k个字符相等,并且pattern[k]不能等于pattern[j]。把这个k值记为next[j]。next[j] 是pattern中通过比较pattern[k]和text[[i]从而可以继续搜索下去的位置。也就是说,将pattern向右滑动,使得pattern[k]和text[i]对齐,并从该点继续搜索。如果没有这样的k存在,令next[j]=-1,让搜索从pattern[0]和text[i+1]开始。(这里相当于将pattern向右滑动来讲不存在的位置-1与text[j]对齐,然后恢复搜索)


假设我们已经得到next数组,那么KMP算法步骤如下:

说明:假设要在文本串text中查找模式串pattern。文本存储在text的0~n-1的单元中,模式存储在 pattern 的0~-m-1中。

输入: 串text和pattern

输出:如果在text中找到pattern,则返回pattern在text中的起始位置,否则,返回-1

               (1)初始化index, i和j为0;index是text和pattern比较的起始位置,i扫描text,j扫描 pattern;

(2)当i<n且j<m时,

若text[i] =pattern[j],则i和j都增1;

否则,做一下工作:

将pattern向右滑动合适的距离, index=index+j-next[j];

如果next[j]不等于-1, j = next[j];否则 j等于0,i增1。

(3)如果j<m,则index等于-1;

(4)返回index。

4. Next数组

KMP算法的关键在于如何得到next数组。根据图2,我们可以直观地看出,next[j]就是pattern中最长的并且和pattern[j]前k个字符匹配的前缀pattern[0],pattern[1],...,pattern[k-1],而且pattern[k]不等于pattern[j]。事实上,next中保存的是模式串中所有有相同前缀和后缀的子串的前缀后缀长度。这些前缀后缀相同的子串可以将模式串pattern在其自身的一个副本上滑动来得到。再次考虑一个普通形式的模式串{p0, p1, ..., pm-1}。将它与自身的副本进行模式匹配。如图3所示,显然next[0]=-1,因为pattern[0]没有前缀。现在,如果next[0, next[1], ..., next[j-1]已经得到,可以使用这些值来计算next[j]。


KMP3

                                图3 模式串与其自身的一个副本进行模式匹配

如果前缀长度为k,并且pattern[k]不等于pattern[[j], 那么根据next的定义,next[j] = k。如果pattern[k]等于pattern[[j],显然next[j]  = next[k]。根据以上分析,得到计算next的方法如下:

输入:串pattern

输出:数组next

(1)初始化next[[0]=-1, k为-1,j为0;

(2)当j<m时,

如果k不等于-1并且pattern[k]不等于pattern[j],k=next[k];

k和j增1;

如果pattern[j]==pattern[j],则next[j]=next[k];否则,next[j] = k。

计算next的代码如下:

void GetNext(char* pattern, int next[])
{
	assert(next);
	next[0] = -1;
	int k = -1;
	int j = 0;
	int m = strlen(pattern);

	while(j<m)
	{
		if(k != -1 && pattern[k] != pattern[j])
			k = next[k];

		k++;
		j++;

		if(pattern[j] == pattern[k])
			next[j] = next[k];
		else
			next[j] = k;
	}
}

有了next数组,就可以得到KMP的完整算法。KMP完整的C代码如下:

int KMP(char* text, char* pattern)
{
	int n = strlen(text);
	int m = strlen(pattern);
	int index  = 0; //index 是ptn在text中的其实位置
	int i = 0;
	int j = 0;
	int* next = (int*)malloc(m*sizeof(int));
	GetNext(pattern, next);
	
	while(i<n&& j<m)
	{
		if(text[i] == pattern[j])
		{
			i++;
			j++;
		}
		else
		{
			index = index + j - next[j];
			if(-1 != next[j])
				j = next[j];
			else
			{
				j = 0;
				i++;
			}

		}
	}

	if(next)
	{
		free(next);
		next = NULL;
	}

	if(j == m)
		return index;
	else
		return -1;
}

5. 总结

KMP算法的关键在于求解next数组。借助next数组,可以避免暴力搜索算法中在失配时的回溯,降低算法时间复杂度,提高搜索效率。分析得知,暴力搜索算法的时间复杂度为O(mn), 而KMP为O(m+n)。明显O(mn)>>O(m+n)。在理解KMP的过程中,首先应该从暴力搜索算法开始,理解为什么暴力算法会产生回溯和如何避免回溯问题。对于next数组,需要深刻理解“具有相同前缀和后缀的子串”这一概念。动手一步步迭代算法的过程是最好的。

6. 参考文献

[1] http://write.blog.youkuaiyun.com/postedit?ref=toolbar<http://write.blog.youkuaiyun.com/postedit?ref=toolbar>

[2] C++数据结构导引

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值