KMP算法的实现

         KMP算法,字符串匹配问题,都是老生常谈了。但就论及我手头上的《算法导论》而言,KMP算法这一章节讲得实在是太烂(或许是翻译的问题)。我花费了很大精力去琢磨前缀函数的意义,却始终不得其法。无奈之下恰好Google出了matrix67的一篇详解,终于理顺了思路,在此推荐其文:KMP算法详解

         为什么KMP算法如此鼎鼎大名?答案无他,一者简单,二者高效。短短十几行,就能在O(N)的时间上界完成字符创匹配。但是精妙的KMP算法理解起来却颇有难度:为什么KMP算法是正确的?为什么时间复杂度仅是O(N)?不弄清楚这两个问题,还真不能说理解了KMP算法。

 

为什么KMP算法是正确的?

         相对朴素字符串匹配而言,KMP算法是如何能大幅提升效率而又保持其正确性的呢?答案是引入了前缀数组π。虽然我们假设输入的文本text和模式pattern都是随机的,但是通过对pattern的预先处理之后是能在匹配时期提升效率的。

假设pattern的字符串长度为m,我们对q,(1 <= q <= m)依次求出π[q]的值。

 

我们定义: π[q] = max { k: k< q && Pk 后缀于Pq }, Pk,Pq分别为pattern的前k位和前q位子串。

         对于字符串“aabaab”而言,π[1]显然为0,因为空串必然后缀于P1 = ”a”。因为子串P1 = “a” 后缀于P2 = “aa”,并且1也是π[2]的最大k值,所以π[2] = 1。对于P3 = “aab”,只有空串后缀于P3,则π[3] = 0。同理依次可以推倒出,π[4]= 1,π[5] = 2,π[6] = 3。

 

         那么计算出的前缀数组在匹配过程中如何使用呢?对于1 <= i <= n,1 <= j <= m,如果text[i]和pattern[j + 1]相等,我们就不需要使用前缀数组,直接更新当前的匹配状态为j + 1即可。如果text[i]和pattern[j + 1]不相等,我们置j = π[j]直到j == 0 或者text[i]和pattern[j + 1]相等为止。之所以能这样做,是因为前缀数组保证了Pπ[j]后缀于Pj,如果当前的状态j不能匹配,我们向前倒退至状态π[j]直到能够完全匹配。当匹配状态j = m时,即在text中发现能完整匹配pattern的子串输出即可。

 

         与其说KMP算法为什么这么快而且正确,我们不如分析为什么朴素字符串匹配这么慢的原因。在朴素字符串匹配的过程中,每一次与pattern的匹配都是从头开始,这样时间复杂度必然是O(M(N – M + 1))。但是KMP算法利用了前缀数组的特性,不用每次发现不匹配的时候都放弃之前全部的工作,而是向前倒退一定的幅度,尽量利用已经完成的匹配状态。因为不用每次都推倒重建,所以KMP算法自然也快得多。

         至于对KMP算法的更严格证明,其实可归纳于自动机的证明,在此也不引出了。


为什么时间复杂度仅是O(N)?

         个人认为matrix67的讲解十分透彻,所以就直接引用了。


j:=0;
for i:=1 to n do
begin
   while (j>0) and (B[j+1]<>A[i]) do j:=P[j];
   if B[j+1]=A[i] then j:=j+1;
   if j=m then
   begin
      writeln('Pattern occurs with shift ',i-m);
      j:=P[j];
   end;
end;


         为什么这个程序是O(n)的?其实,主要的争议在于,while循环使得执行次数出现了不确定因素。我们将用到时间复杂度的摊还分析中的主要策略,简单地说就是通过观察某一个变量或函数值的变化来对零散的、杂乱的、不规则的执行次数进行累计。KMP的时间复杂度分析可谓摊还分析的典型。我们从上述程序的j值入手。每一次执行while循环都会使j减小(但不能减成负的),而另外的改变j值的地方只有第五行。每次执行了这一行,j都只能加1;因此,整个过程中j最多加了n个1。于是,j最多只有n次减小的机会(j值减小的次数当然不能超过n,因为j永远是非负整数)。这告诉我们,while循环总共最多执行了n次。按照摊还分析的说法,平摊到每次for循环中后,一次for循环的复杂度为O(1)。整个过程显然是O(n)的。

 

         最后贴出自己的代码,欢迎读者指正。


#include <iostream>
#include <vector>
#include <string>

using std::cin;
using std::cout;
using std::endl;
using std::vector;
using std::string;

void compute_prefix_function(const string& pattern, vector<int>& state);
void kmp_matcher(const string& text, const string& pattern);

int main()
{
	string text;
	cin >> text;
	string pattern;
	cin >> pattern;
	kmp_matcher(text, pattern);
	return 0;
}

void kmp_matcher(const string& text, const string& pattern)
{
	int n = text.size();
	int m = pattern.size();

	vector<int> state(m + 1);
	compute_prefix_function(pattern, state);
	int q = 0;
	for (int i = 0; i != n; ++i) {
		while (q > 0 && text[i] != pattern[q]) {
			q = state[q];
		}
		if (text[i] == pattern[q]) {
			++q;
		}
		if (q == m) {
			cout << "pattern occurs with shift " << i - m + 1 << endl;
			q = state[q];
		}
	}
}

void compute_prefix_function(const string& pattern, vector<int>& state)
{
	int m = pattern.size();
	state[1] = 0;
	int k = 0;
	for (int i = 1; i != m; ++i) {
		while (k > 0 && pattern[i] != pattern[k]) {
			k = state[k];
		}
		if (pattern[i] == pattern[k]) {
			++k;
		}
		state[i + 1] = k;
	}
}


评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值