关于KMP(Knuth-Morris-Pratt)算法出现的前因后果,其可以解决的问题以及带来的潜在效率提升,在书本中和网络上可以找到的资源实在是太多了,本文不再赘述。
这篇文章的主要目的,是想结合自己实际总结和心得,从另外一个角度,进一步解释KMP算法匹配中主串的指针不需要回退的原因。
背景
在之前学习KMP算法的过程中,经常看到会以一些例子演示进行说明,比如下例,当前匹配进行到字符'e'处,失败了。
a b a d e
f g h i
a b a d x
a
接下来,一般解释都会继续使用此类例子来说明,匹配可以直接从当前主串'e'处开始,即主串的指针不需要回退,只需要模式串将指针进行移位到某其位置来继续匹配就可以了。
这种演示的说明方式确实易懂,可操作性强。但是对我个人来说,很难让我完全抛开例子,从理论/通用性的角度上去理解为什么看起来主串的指针不需要回溯。
比如现在有一个主字符串T[t1, t2, t3,..., tn],模式串P[p1, p2, p3,...pm],主串和模式串的指针在第一次匹配时,都匹配到了k+1的位置,即T和P的前k个字符都是1:1匹配的,
t1 t2 t3 ... tktk+1 ... tn
p1 p2 p3 ... pkpk+1 ... pm
在没有具体字符出现的情况下,要怎么样解释T在k+1处的字符串指针不需要回退到第2个字符处?
这个疑问萦绕在我脑子中很久,我也试图通过查找大量的资料,包括《算法导论》,以及KMP三位作者共同发表的论文《Fast pattern matching in strings》,可惜要么基本是一笔带过,要么就是限于本人资质,没有能明白其中的说明。
直到我在Quora上看到了一篇文章关于KMP解释的文章,虽然其中主要是围绕如何查找next(j)来进行的说明,但是给我提供了另外一种思路去理解和解决我的疑问。
如果也有其他的同学也有与我一样的疑惑,希望我的这篇文章能够帮助你。
进入正题
简单说一下本文涉及到的一些术语定义:
模式串P。即通常在字符串搜索中提到的子串。长度为m,指针指示当前字符的位置定义为j。
主串T。即被匹配字符串,在该字符串中查找能够完全匹配上模式串的起始位置。长度为n (n > m),指针指示当前字符的位置定义为i。
i和j第一次匹配都从1开始进行计算。即t1代表T中的第一个字符,p1代表P中的第一个字符。
假定之前已经进行了数次匹配并失败,当前一次的匹配是从主串的任意位置k+1处开始的,并且主串和模式串均成功匹配连续L个字符。到主串的位置(k+L+1)处匹配失败。
那么有
k + L + 1 = i(其中i < n); L + 1 = j (其中j < n)
t1 t2 t3 ... tk | tk+1 tk+2 tk+3 ... tk+L | tk+L+1 | ... ... ... tn |
---|---|---|---|
p1 p2 p3 ... pL | pL+1 | ... pm |
我之前的疑惑是,如果主串指针不从(k+L+1)回溯到(k+2)的位置,万一k+2处开始模式串和主串能够匹配上,那这种成功匹配的场景,不就可能被遗漏了么。
现在我们来看看,假定在(k+2)处模式串和主串真的有成功匹配的可能,那么基于我们已知的信息,必然需要如下内容成立:
[p1, p2, p3, ..., pL-1 ] == [tk+2, tk+3, tk+4, ..., tk+L]
继续分析,
Alt1: 假如我们的主串和模式串确实能够满足上述条件,那么接下来我们需要进行匹配的,是pL 与tk+L+1,看到了么,此时主串的指针到了回溯前的位置;
Alt2:假如我们的主串和模式不能够满足上述条件,那么此时主串指针回溯到(k+2)的位置匹配过程可以结束了,位置指针可以重新指到(k+3)的位置,并且模式串重新回溯到1的位置,继续看是否有主串和模式串成功匹配的可能性。
我们再来看看,假定在(k+3)处模式串和主串真的有成功匹配的可能,那么基于我们已知的信息,必然需要如下内容成立:
[p1, p2, p3, ..., pL-2 ] == [tk+3, tk+4, tk+5, ..., tk+L]
怎么样,是不是觉得很熟悉,这是一个重复上述Alt1和Alt2分析的过程,最终的结果是两种,要么是此时主串的指针还是到了k+L+1的位置,要么就是进入主串指针进入下一轮的从(k+4)的位置开始匹配的过程。
如果上述过程能够不断的重复进行下去,始终没有发现匹配的,直到主串指针指到(k+L),模式串指针再一次重新回溯到了1的位置,现在我们需要判断如下等式是否成立的时候了:
[p1] == [tk+L]
又一次,我们的分析结果:如果成立,那么下一个主串和模式串要匹配的字符,就在主串指针的(k+L+1)的位置,与模式串的t2进行匹配,而此时主串的位置,正是最初从k位置开始匹配时,匹配失败的位置;
如果等式不成立,那么主串指针自然指向(k+L+1)的位置,而模式串需要使用p1 与 tk+L+1开始进行匹配。
结论
从上述过程的分析,我们可以看出,如果主串从k+1的位置开始与模式串进行匹配,成功匹配了L个连续字符后,主串的指针不需要回退,其实是因为:
我们假设可以有多轮的隐含回溯匹配过程,通过利用已知的信息[p1, p2, p3, ..., pL ] == [tk+1, tk+2, tk+3, ..., tk+L],我们可以完全推断处,不管这些轮匹配中,是否至少有一轮在主串的(k+L+1)字符之前,使得所有字符与有效的模式串字符匹配成功,最终都会使得主串指针指向(k+L+1),即i这个位置。
那么我们要做的,就是相对来说,不需要指针i在主串中回溯,通过调整模式串的位置指针j的位置变化(next(j)函数),来达到快速匹配的效果。