关于KMP(Knuth-Morris-Pratt)算法出现的前因后果,其可以解决的问题以及带来的潜在效率提升,在书本中和网络上可以找到的资源实在是太多了,本文不再赘述。

这篇文章的主要目的,是想结合自己实际总结和心得,从另外一个角度,进一步解释KMP算法匹配中主串的指针不需要回退的原因。

背景

在之前学习KMP算法的过程中,经常看到会以一些例子演示进行说明,比如下例,当前匹配进行到字符'e'处,失败了。

a b a d e f g h i

a b a d x a

接下来,一般解释都会继续使用此类例子来说明,匹配可以直接从当前主串'e'处开始,即主串的指针不需要回退,只需要模式串将指针进行移位到某其位置来继续匹配就可以了。

这种演示的说明方式确实易懂,可操作性强。但是对我个人来说,很难让我完全抛开例子,从理论/通用性的角度上去理解为什么看起来主串的指针不需要回溯。

比如现在有一个主字符串T[t, t, t,..., t],模式串P[p, p, p,...p],主串和模式串的指针在第一次匹配时,都匹配到了k+1的位置,即T和P的前k个字符都是1:1匹配的,

t t t ... tt ... t

p p p ... pp ... p

在没有具体字符出现的情况下,要怎么样解释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开始进行计算。即t代表T中的第一个字符,p代表P中的第一个字符。

假定之前已经进行了数次匹配并失败,当前一次的匹配是从主串的任意位置k+1处开始的,并且主串和模式串均成功匹配连续L个字符。到主串的位置(k+L+1)处匹配失败。

那么有
k + L + 1 = i(其中i < n); L + 1 = j (其中j < n)

p p p ... pp... p

我之前的疑惑是,如果主串指针不从(k+L+1)回溯到(k+2)的位置,万一k+2处开始模式串和主串能够匹配上,那这种成功匹配的场景,不就可能被遗漏了么。

现在我们来看看,假定在(k+2)处模式串和主串真的有成功匹配的可能,那么基于我们已知的信息,必然需要如下内容成立:

[p, p, p, ..., p ] == [t, t, t, ..., t]

继续分析,

Alt1: 假如我们的主串和模式串确实能够满足上述条件,那么接下来我们需要进行匹配的,是p 与t,看到了么,此时主串的指针到了回溯前的位置;

Alt2:假如我们的主串和模式不能够满足上述条件,那么此时主串指针回溯到(k+2)的位置匹配过程可以结束了,位置指针可以重新指到(k+3)的位置,并且模式串重新回溯到1的位置,继续看是否有主串和模式串成功匹配的可能性。

我们再来看看,假定在(k+3)处模式串和主串真的有成功匹配的可能,那么基于我们已知的信息,必然需要如下内容成立:

[p, p, p, ..., p ] == [t, t, t, ..., t]

怎么样,是不是觉得很熟悉,这是一个重复上述Alt1和Alt2分析的过程,最终的结果是两种,要么是此时主串的指针还是到了k+L+1的位置,要么就是进入主串指针进入下一轮的从(k+4)的位置开始匹配的过程。

如果上述过程能够不断的重复进行下去,始终没有发现匹配的,直到主串指针指到(k+L),模式串指针再一次重新回溯到了1的位置,现在我们需要判断如下等式是否成立的时候了:
[p] == [t]

又一次,我们的分析结果:如果成立,那么下一个主串和模式串要匹配的字符,就在主串指针的(k+L+1)的位置,与模式串的t进行匹配,而此时主串的位置,正是最初从k位置开始匹配时,匹配失败的位置;
如果等式不成立,那么主串指针自然指向(k+L+1)的位置,而模式串需要使用p 与 t开始进行匹配。

结论

从上述过程的分析,我们可以看出,如果主串从k+1的位置开始与模式串进行匹配,成功匹配了L个连续字符后,主串的指针不需要回退,其实是因为:

我们假设可以有多轮的隐含回溯匹配过程,通过利用已知的信息[p, p, p, ..., p ] == [t, t, t, ..., t],我们可以完全推断处,不管这些轮匹配中,是否至少有一轮在主串的(k+L+1)字符之前,使得所有字符与有效的模式串字符匹配成功,最终都会使得主串指针指向(k+L+1),即i这个位置。

那么我们要做的,就是相对来说,不需要指针i在主串中回溯,通过调整模式串的位置指针j的位置变化(next(j)函数),来达到快速匹配的效果。

参考文献

03-05 15:32