浅谈KMP算法

一、定义

KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人们称它为克努特—莫里斯—普拉特操作(简称KMP算法)。KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是通过一个next()函数实现,函数本身包含了模式串的局部匹配信息。KMP算法的时间复杂度O(m+n)。

在这里我们一定要用数学的思维去约束自己,KMP算法其实就是披了一层推理外衣的BF算法罢了。

二、算法理解

1、简单思路分析

我们可以从这样一个问题开始入手。
比如说我们想要将目标串"abcdabd"和目标串"abd"进行匹配,
target–:abcdabd(“–”只是为了占一个格子,并无实际意义)
pattern:abd
第一次比较失败,因为target[2]!=pattern[2]。那么按照传统意义上的BF算法来说,我们第二次应该进行这样的比较:
target–:abcdabd
pattern:–abd
也就是target回溯,pattern也要回溯。
那么我们究竟有没有必要进行这次回溯呢?
我们可以利用数学知识进行证明,
target[0]==pattern[0];
target[1]==pattern[1];
target[0]!=target[1];
那么target[1]!=pattern[0];
所以我们没有必要进行target[1]与pattern[0]的比较,可以直接跳到target[2]与pattern[0]的比较。
在这里我们需要明白一点,就是为什么直接跳到target[2]与pattern[0],而不是其他元素之间的比较呢?
这是因为在刚刚的证明中,我们已知的条件只有四个,用三个条件我们的出了上述的一个结论,而target[1]!=pattern[0]会使我们的演算跳到target[2]与pattern[0]之间的比较,但是最后一个已知条件是target[2]!=pattern[2],从这个条件中我们没法得出target[2]与pattern[0]之间的关系,所以我们没法跳过。
从这个例子本身我们应该可以隐隐约约的感受到KMP算法的雏形,它似乎有点像学会利用了数学知识的BF算法一样。
假如我们在列举几个类似的例子,我们也都可以发现它们都可以这样跳过去。详细过程应该是可以通过严谨的数学证明过程证明出来的。但是我们不是一个数学家,所以我觉得我们没有必要进行这样的证明。我们就当是一个约定俗成的结果吧,就像BF算法一样自然。

接下来我们再举一个例子,target串是"abcabcabddd",pattern串是"abcabd"。
target–:abcabcabddd
pattern:abcabd
在这里我们进行了第一次比较,但是令人遗憾的是target[5]!=pattern[5],那么我们是否可以像是上一个例子那样,直接将target[5]与pattern[0]进行比较呢?如果我们仔细观察的话我们可以发现,从target串的第3个元素开始,pattern==(target的子串),如果我们直接跳到target[5]的话,那它将失配。
在这里我们需要思考为什么会出现这样的一种情况呢?通过观察我们可以知道,target[0-1]==target[3-4]==pattern[0-1]==pattern[3-4]。
或者说,假如我们利用数学知识去推导的话:
target[0]==pattern[0];
target[1]==pattern[1];
target[2]==pattern[2];
target[3]==pattern[3];
target[4]==pattern[4];
target[5]!=pattern[5];
pattern[0]==pattern[3];
pattern[1]==pattern[4];
我们可以得出如上的结论
当我们进行逻辑上的比较时,target[1]!=pattern[0];所以进行下一次比较,target[2]!=pattern[0];所以我们再进行下一次的比较,target[3]==pattern[0],所以我们进入这次比较,target[4]==pattern[1];那么在进行target[5]与pattern[2]进行比较的时候,我们的已知条件只有target[5]!=pattern[5],除此之外,对于target[5]一无所知,所以我们不可以跳过。
从上述的思路,我们可以知道下一次的比较,应该是target[5]和pattern[3]。
问题又来了,为什么会出现这种情况呢?
从数学的逻辑上来看,是因为我们缺乏已知的条件。而我们缺乏的条件正是由于pattern模式串中具有相同元素引起的。
我们再多举几个类似的例子我们会发现它确实有这么一个规律,即若是有相同的元素,我们模式串应该跳到第一个相同子串的后面第一个元素。
那么这里其实还有一个问题就是假如我们的模式串是"acdfcdb",那么其中的cd算不算相同的子串呢?
其实我们再举几个例子我们就知道不算,它是第一个例子的变体。可以肯定KMP算法是可以看作基于BF算法的,它是戴了一双名为数学的眼镜的BF算法。(实际上数学方法帮我们比较过了)
在这里我们再引入一个概念,即最长前缀字串和后缀子串,前缀子串它一定包括第一个元素,后缀子串它一定包括最后一个元素。为什么是最长呢?我们再举一个例子,“aaa"的相同字串有"a"和"aa”,我们选择的是"aa",有兴趣的可以举一个例子来尝试一下,在这里就不多赘述了。
其实从上面的几个例子和我的言辞中,我想应该都可以明确,跳到哪一个位置实际上与目标串无关,与模式串有关。模式串如果前缀子串与后缀子串相等的话,我们就会类似于第二个例子;如果不相等的话,我们就参照第一个例子。

那么我们假如说要转化为代码的话该怎么办呢,难道每一次比较都要查看模式串是否有相同的前缀和后缀字串吗?
其实思想是这么一个思想,实际上去实现的时候我们应该用封装的思想,将模式串提前进行一次模拟,得到next数组。
什么叫next数组呢,我们可以这样理解:在例一中,假如我们在比较到最后一个元素的时候,有一个数组对应这个元素值,它指向的元素是假如比较失败的话我们下一步应该跟谁比较,这样不就好了吗?

2、具体实现

我们先假设我们有了这样的一个数组,下面我们进行比较:
abcabd
abd
target[2]!=pattern[2],所以根据next数组,我们下一步应该target[2]与pattern[next[2]]进行比较。

我们具体应该怎么样去具体的构造这个next数组呢?
我们举一个例子:
pattern=“abababcac”;
那么假如说在和target串比较的过程中,在第0个元素中失败了,那么我们假设next[0]=-1;因为它连第一步都没有成功,直接做一个标记,代表让target向后。假如说在第一个元素失败了,那么next[1]=0,让它指向第0个元素重新进行比较,这是第一个例子;。。。
这样一直持续到第四个元素失败,此时next[4]=1,这是第二个例子;。。。过程就是这样。
那么从上面我们应该可以总结出来,求next数组实际上就是求pattern串在第i个元素之前是否有前串和后串相等的情况。若是有,那么next[i]=j(j是前串的后第一个元素的序号)

三、代码实现

OK,从上面这么多例子我们应该可以模糊的知道这个算法大致的意思了,接下来应该让我们手动去实现了。
我们从一个例子入手,pattern=“ababac”
我们记第一个指针指向pattern的第0个元素,第二个指针指向第2个元素,在这里其实我是借鉴了小甲鱼的那个容易理解的思路去讲的,我们就这样先从容易理解的成功的情况开始:

if(pattern.charAt(i)==pattern.charAt(j)){
	i++;
	j++;
	next[j]=i;//我们是将下一个元素的next数组给求了出来,这点千万要记得
}

然后假如我们失败了就像假如说i=3,j=5

if(pattern.charAt(i)!=pattern.charAt(j)){
	j=next[i];
}

在这里肯定有很多人不理解这是为什么,其实我们可以这么想,我们在第一次比较成功的时候相当于pattern[0-1]去和pattern[2-3]进行比较,然后如果相等,我们又进行了下一次的比较,但是我们是不是也可以看作pattern[0-2]和pattern[2-4]进行比较呢?这本质上还是一个模式串的匹配问题,只不过是自己和自己比较罢了,那么假如失败的话,我们应该按照KMP算法去转向对应的位置,如果还有人不理解的话,就需要多举几个例子,剖析他们的本质。

OK,然后我们将代码合起来。
这是我们代码合起来以后的样子:

int i=-1;j=0;
int[] next=new int[pattern.length()];
if(i==-1||pattern.charAt(i)==pattern.charAt(j)){
	i++;
	j++;
	next[j]=i;
}
else{
		j=next[i];
	}

这里有人会问了,为什么一开始我们要把i=-1呢?
其实实现方式有很多种,我们只要把握思想就好了,我们上面已经写了几个简单的步骤,合起来以后,我们想要代码不轻易改动的话,这样是最优解了。
然后我们再运行一遍,看是否是我们想要的结果,如果不是我们想要的结果,那么一般来说都是边界值没有处理好造成的,稍微加几个限制条件就可以了。

然后就是KMP算法的框架了,这个我就不叙述了,KMP算法的灵魂就在求这样一个智能的next数组上面,其余的代码,几乎和BF算法一样,没必要多做叙述。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值