众所周知,KMP算法是字符串匹配算法。
那么首先,我们来看看一般字符串匹配算法的过程:
一般字符串匹配算法(暴力匹配)
如图。
两个字符串T和P,我们传统做法便是这样:
1.首对齐,一个一个对比,碰到了不匹配的:
2.待检测字串往后移一个,再重复同样的事情:
到现在,我们用一般匹配算法(暴力匹配)完成了这个字符串匹配过程。
我们总结一下一般匹配算法:暴力算法匹配过程中,我们首先会对T[0]和P[0]进行比较,如果相同则匹配下一位,知道出现不相同的情况,此时我们会丢弃到已经匹配的情况,重新让p[0]和T[1]对齐进行匹配,重复上述过程,知道匹配成功或主串遍历结束。我们发现,这种过程很繁琐,这种效率很低。
举个例子:
如图,主串是aaaaaaaaaab,模式串是aaaab,用一般匹配算法的话,在与主串中每一位对齐时都要判断五次,那么总共的判断就需要35次匹配(自己手动模拟一下哦)。
————————————————手工分割线——————————————
为了提高算法的效率,我们对字符串一般暴力匹配法进行优化,这样就遇到了我们今天的主角。
KMP算法
首先,在看KMP之前,我们需要知道一个叫前缀表的东西。前缀表究竟是什么呢?拿这个模式字符串P来说吧。
- 首先,我们看看前缀是什么?
如P:a b a b c所有的前缀:
a
a b
a b a
a b a b
a b a b c
像这样,从头开始,在字符串中某一位置停止的子字符串,就是前缀
同理,后缀就是从后开始,在字符串中某一位置停止的子字符串
P:a b a b c所有的后缀:
c
b c
a b c
b a b c
a b a b c - 其次,我们要对前缀中的每个前缀求最长公共子前后缀。
a------------没有子前后缀,所以为0
a b----------最长是一个元素:a和b,不是公共的。因此,最长公共子前后缀也是0
a b a--------首先最长是两个元素:a b 和b a 是最长的,但不是公共的。所以我们看一个元素的:a和a,好了,最长公共子前后缀是1
a b a b-----首先最长是三个元素,明显,a b a和b a b 不是公共的,所以减少一个元素:a b和a b,好了,最长公共子前后缀是2
a b a b c----首先最长是四个元素,明显,abab和babc不同。所以减少一个元素:aba 和abc 同样不是公共的。再减少一个元素 :ab和bc,也是非公共的。再减少:a和c,也是非公共。因此,这个串的最长公共子前后缀是0
好,到现在,我们求出来了这个前缀表每行的最长公共前后缀:
a --------------0
a b ------------0
a b a----------1
a b a b -------2
a b a b c------0 - 最后,我们的前缀表就是去除最后一行,再往前方增加一个-1:
把绿色这一段,结合模式串P,像这样写出来:
a | b | a | b | c |
---|---|---|---|---|
-1 | 0 | 0 | 1 | 2 |
这个表,就是前缀表。
———————————————手工分割线——————————————
现在,我们看看如何用刚刚求得的前缀表运行KMP算法。
注:红色是模式串的下标,蓝色就是我们刚才所求的前缀表里的数据。
1.进行第一次匹配。
.
T[3]和P[3]匹配失败。
匹配失败的时候,我们就要看前缀表,当前导致匹配失败的是P[3],前缀表显示的是1,所以我们把P[1]和当前匹配失败的T[3]位置对齐。
对齐之后如上图。
2.进行第二次匹配。
上一步做完之后,当前是T[3]和P[1]对齐,那么现在就由当前对齐位置开始,逐步往后匹配。 也就是说,在当前对齐位置之前的元素,不需再对它进行匹配了(原因会在后面解释,现在就关注KMP的过程)。
发现此时匹配失败,根据上面的方法,此时前缀表显示的0,所以把P[0]与匹配失败的位置对齐。
对齐之后如上图。
3.同2,由当前对齐位置开始,逐步往后匹配。
.
匹配失败,此时前缀表显示0,继续把P[0]与匹配失败的位置对齐。
对齐之后如上图。
4.同上,从当前对齐的位置开始,逐渐往后匹配。
.
匹配失败,此时发现前缀表显示的-1,那么聪明的我们就知道,现在需要把P[0]的前一位与匹配失败的位置对齐。
对齐之后如上图所示。
5.从当前匹配失败位置开始,逐步往后匹配。
我们发现,对齐的位置是P[-1],这要怎么对比。很简单呀,我们跳过它从下一位开始嘛。
此时,全部匹配成功了,此时,KMP程序结束。
————————————————手工分割线——————————————
现在,我们拿出刚才的极端例子:
第一步:计算前缀表;
a ------------0
a a ----------1
a a a--------2
a a a a-------3
a a a a b----0
前缀表:
a | a | a | a | b |
---|---|---|---|---|
-1 | 0 | 1 | 2 | 3 |
第二步:进行匹配;
.
到此结束,我们统计一下,总共对比了19次,相比之前的35,有了很大的优化。
————————————————手工分割线——————————————
到现在为止,我们已经感受了整个KMP算法的过程。
现在,我们来总结一下
KMP算法的基本思路:一般情况下,当字符串匹配失败之后,我们会让模式串后移一格,之后重头再做匹配。而KMP算法则利用到了匹配失败后产生的信息(当前前缀表中显示的数字)。当匹配失败之后,KMP算法便会知晓,某个前缀不需要再次进行匹配操作(例如上述极端例子中的前缀:a a a( P[0]P[1]P[2] ))。因为它们已经是匹配好的。然后利用前缀表,让当前前缀表所指示下标数字的模式串元素与当前匹配失败位置对其,从当前位置开始,逐步往后进行匹配。(可能后面这句话有些拗口,难懂。通俗一点就是:当前位置匹配失败了,我就找现在前缀表指向的模式串元素,以它为准,与主串对齐,再从此模式串元素处开始,逐个往后匹配)
通过上面一段话,我们已经了解到了前缀表的具体作用了。但是肯定会有疑问,为什么前缀表具有这样的作用呢?也就是对前缀表的原理会一头雾水。本人才疏学浅,仅有自己的一点点感受,如下提及,望各位大佬指教!
我们不妨来深层跟踪一下前缀表的使用过程:
如图,我们再当前位置失配。当前前缀表是1。这个1,就是串(a b a)的最大公共前后缀:
串1(a)=串2(a);我们发现,因为能走到失配位,所以失配位之前都是匹配的。当前失配位的上一位是匹配的,而上一位又和P[0]相同,所以P[0]不需要匹配,只需要从P[1]往后遍历。
这个例子可能不太好理解,相信看了下一个例子,一定会对前缀表有所理解的。
再举个例子:
当前前缀表显示是3,也就是串(a a a a)的最长公共子前后缀是3(如图):
我们来分析:
棕色串A和紫色串B是当前失配位所示(3)对应的最长公共子前后缀。当然这两个串(A、B)肯定是相同的啦(这是最大公共子前后缀嘛)。那么因为我们已经对比到了P[4],那么之前的都是匹配成功的了,那便是说,蓝色串C和紫色串B完全相同。咦!是不是发现了什么:串A和串B是相同的,串C和串B完全相同,那么A=B=C,所以A串和C串也就匹配了呀!咦!这样我们就不需要再匹配这这串了,所以让P[3]和刚才的失配位进行对齐,再从P[3]开始,往后匹配判断就行了啊!
好了,以上就是本人对KMP浅显的理解,有不足之处希望大佬指正。
还有下篇:KMP算法----代码实现篇;希望各位大佬亲临指正。