最近刚好回看了一下KMP算法,对KMP算法有了更清晰的理解,于是在这里记录一下,将自己的理解记录归纳。
前置
待搜索串: Sn=S0S1S2...Sn
。如:abbaabbaaba
待匹配串:Pm=P0P1P2...Pn
。如:abbaaba
从暴力匹配讲起
暴力匹配: 对 待搜索串每个位置,都进行待匹配串的搜索,当前位置不匹配,则到下一个位置进行查找。即,从S0开始查找Pm,查不到则在S1再查Pm,直到 S(n-m)的位置,Pm每次都从P0开始匹配。
最差时间复杂度:遍历 n-m 的位置,每个位置进行 m 次比较。 即, (n-m) * m
例如: Sn= aaaaaaaaab,Pm=aab
说明:暴力匹配思路比较清晰简单,因此代码实现也比较简单,在 子串很小的时候是有用的,例如m=1
(即查询单个字符)。
优化方向:减少已经做过的事。如:走过的S位置不要再走(在前部分匹配时)即不回溯S,匹配过的不要每次都从P0位置开始匹配。
第一次优化----不回溯S
在暴力匹配中,如 Sn= aaaaaaaaab,Pm=aab
,可以发现,一开始从S的位置,每次匹配完,匹配到P的最后一个位置才不匹配。此时假设S的位置为x,S已经走了x+m了,但是还是会回到x+1处,重新匹配(即发生了回溯)。
这是一个浪费的地方,P越长,前面匹配得越多越浪费。
假设P已经匹配的位置为y ,我们能不能从 S的x+y处位置接下去比较,即S不再发生回退。
顺着S不发生回退的方向,就可以进行第一次的优化。
考虑一下:其实,S 从 x+y 位置的字符是什么我们已经知道了的。即子串P的0到y的位置,所以S即使不回退,我们也有了 S(x+y)的知识,我们接下来就应该利用这些知识让S不回退。
再考虑一下:假设S从x+1的位置开始匹配了P,最后还是需要比较Sx+y的位置的。
那么此时,匹配S(x+y)应该与P(y-1)进行比较,那么,我们可以先进行 S(x+y)与P(y-1) 的比较。
如果 S(x+y)与P(y-1) 不匹配,那么 S(x+1)的位置已经不可能匹配子串(因为最后一个字符不等),
同样的方式判定S(x+2),S的x到y的位置都可以这样子判定,如果匹配,就要进行另外的展开判断。
如果 S(x+y)与P(y-1)匹配,如果我们能通过已有的条件进行验证S(x+1)到S(x+y)和P(0)到P(y-1)接下来是否匹配,那么,我们就确实不再需要进行 S的回退了。
那么,我们能够验证吗?还知道我们前面考虑吗,我们是拥有 Sx到S(x+y)的知识的。即接下来要知道的Sx到S(x+y)的知识我们都有,所以我们是可以验证。
为了便于理解,我们假设n=10,m=5,x=0,y=4
,由于我们不回退,假设S1
是可能是合法匹配的开始(即x=1
),并验证了 S4=P3
,那么如果 S1S2S3=P0P1P2
,即 可得 S1S2S3S4=P0P1P2P3
而验证 S1S2S3=P0P1P2
是否正确,我们是有办法的,因为我们上一次匹配的知识里面已经知道了S1S2S3
(即 P1P2P3
)
那么如果这一段确实匹配,我们就可以接下去比较S5
和P4
了,而这部分是属于常规的必须的比较操作了。
如果不匹配,那么就是接下去的尝试了,即x=2
时的情况,直到尝试完。最后确实不匹配,S
的位置为S(x+y+1)
,P
的位置从0
开始。
通过这个验证,我们将不用再回退S
,进行了一次优化,虽然现在还需要通过反复比较才能确定不回溯,但是将 S与 P 进行了分离(验证前面的部分是否匹配不再与S有关)。
第二次优化----子串验证加速
由于第一次优化我们已经把 S 和 P 进行了分离(验证前面的部分是否匹配不再与S有关)(后半部分的常规比较是否匹配这一部分是没法少的,不是优化的方向),所以,接下来的优化也与S无关。
在我们操作的过程中,我们发现,可能要重复验证 P 某个范围的值是否相等。
例如:还是上面的例子。由于上一次匹配的x=0,y=4
, 可得: S0S1S2S3 = P0P1P2P3
,即可得 S1S2S3 = P1P2P3
。 而当前要从x=1
匹配要验证 S1S2S3=P0P1P
2 是否正确。 即 S1S2S3=P1P2P3 = P0P1P2
。
那么,下次y
为另外值的时候,又用到这样的比较,即可能 P1P2P3=P0P1P2
经常在重复比较,并且有些比较是多余的,因为我是可以 提前 知道 P1P2P3 !=P0P1P2
的。
所以我们可以提前处理P,产生一个位置对照表(从0到m的最长重复串位置,我们后面再说一下这个对照表怎么产生,现在先跳过)。
这样,下次就可以从y
位置 通过对照表得到另一个位置 y'
,直接比较 S(x+y)与S(y’) 就可以直接知道前面是否匹配,而不用P(y-1)一次次比较,如果不匹配,则y’ 可能有 y’’ ,如果没有,那么说明前面都不可能匹配,进入后续S(x+y+1)
的常规流程。这样,通过预处理对照表,大大减少了子串的比较。
对照表的产生:我把这个表理解成0-m每个位置从0开始的最长重复子串的位置。即:m=y
时,存在i
,使 P0....Pi = P(m-i) P(m-i-1)...Py
,当然,i
不能和y
相等,对照表存放的就是每个位置的i
值。举几个例子。如下,此处i
不存在时记为*
aab
对照表为 *1*
abcabcacab
对照表为 ***0123*01
这里找y
位置 i
的值简单直接一点的算法思路:
y=0时,*
y>0时,假设 i=y-1,那么 Py-1 =Py,Py-2=Py-1...
假设i=y-2,那么 Py-2=Py,Py-3=Py-1..
最后可以得到 i 的确定值。
通过对照表,我们把子串匹配的验证也进行了优化。
KMP算法
KMP算法的基本基本原理即为上面的两次优化。即:
不回溯S,将前部分匹配判断留给子串
用预处理对照表,减少子串判断。
空间复杂度:O(m) 存放对照表。
时间复杂度:O(n+m)