最近又想起了KMP算法,原来一直没搞明白工作原理,现在总算是开点窍了,推荐大家看这篇文章,写的很简单易懂
推荐理由:简单明了,是我看过介绍KMP算法流程的所有文章中,最易懂的一篇(这篇文章仅仅是介绍了KMP算法的工作流程,并没有介绍KMP算法为什么当初这么设计!)
原文地址:http://jakeboxer.com/blog/2009/12/13/the-knuth-morris-pratt-algorithm-in-my-own-words/
===================================================================================
最近我一直在研读各种关于Knuth-Morris-Pratt这种字符串匹配算法的介绍文章。但种种原因导致没有一种我能够真正理解的简简单单的介绍。
最终,在一遍又一遍的读完算法导论中相同段落之后,我决定静下心来好好写一些例子,把自己所理解的KMP算法写出来。现在,我总算明白了这个算法,也能解释明白它了!下面我就用我自己的大白话给大家姐是个明白!不过你需要注意一下,我并不打算解释KMP算法为什么比其他的字符串匹配算法高效,因为在这里已经解释的够清楚的了,而我要做的工作是用自己的话,介绍一下KMP算法的工作流程。
部分匹配表(The Partial Match Table)
KMP的精髓无疑就是部分匹配表了。明不明白KMP本质上就在于明不明白部分匹配表里所有数值的含义,我会尽可能的用简单明了的话进行解释。
下面这个是”abababca”这个模板(pattern)的部分匹配表:
char: | a | b | a | b | a | b | c | a |
index: | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
value: | 0 | 0 | 1 | 2 | 3 | 4 | 0 | 1 |
如果我有一个8个字符的模板(这里就拿”abababca”来举例子),我的部分匹配表会有8列。如果我此时此刻正关注于模板的第八列,即最后一列,那意味着我考虑到了整个模板,(即”abababca”);如果我此时此刻正关注于模板的第七列,那意味着我当前仅仅考虑到了整个模板的前七位,(即”abababc”),此时第八位(”a”)是无关的,不用理睬;如果我此时此刻正关注于模板的第六列,那意味着……看到这里你应该已经明白我的意思了。目前我还没有提到部分匹配表每列数据的含义,在这里仅仅是先交代一下部分匹配表的大概。
现在,为了解释刚刚提到的每列数据的含义,我们首先要明白什么是最优前缀(proper prefixes)什么是最优后缀(proper suffixes):
最优前缀:一个字符串中,去除一个或多个尾部的字符得到的新的字符串就是最优前缀。例如,”S”,”Sn”, ”Sna”, ”Snap”,都是”Snape”的最优前缀。
最有后缀:一个字符串中,去除一个或多个首部的字符得到的新的字符串就是最有后缀。例如,”agrid”, ”grid”, ”rid”, ”id”, ”d”都是 ”Hagrid”的最优后缀。
明白了这两个概念以后,我现在就可以用一句话概括部分匹配表里每列数据的含义了:
模板(子模板)中,既是最优前缀也是最优后缀的最长的字符串的长度。
下面我来验证一下这句话。还拿”abababca”这个模板举例来讲,假设当前我正在关注于模板的第三列数据,如果你还记得我在前文提到的,你应该知道这意味着我们目前仅仅关心前三个字母(”aba”)。在”aba”这个子模板有两个最优前缀(“a”和”ab”),有两个最优后缀(“a”和”ba”),不难看出,最优前缀与最优后缀中,相同的只有”a”这一个,那么此时此刻既是最优前缀也是最优后缀的最长的字符串的长度就是1了。
我们不妨再试一试第四列,第四列的话我们应该是关注于前四位字母(“abab”),这里可以看出有三个最优前缀(“a”,”ab”,”aba”)和三个最有后缀(“b”,”ab”,”bab”),这一次”ab” 既是最优前缀也是最优后缀,并且长度为2,是最优前缀与最优后缀交集里最长的,因此,部分匹配表的第四列的值赋值为2。
如果你还没试够,那你可以再试试第五列的情况,也就是关注于前五位,即“ababa”。这里不难得到4个最优前缀(“a”, “ab”, “aba”, and “abab”)和四个最有后缀(“a”, “ba”,“aba”, and “baba”),其中公共的有两个(“a”和“aba”),对比一下不难看出”aba”长度为3,且比”a”要长,所以部分匹配表的第五列赋值为3。
跳过中间的,直接来看第七列吧,此时只考虑前七位字母(“abababc”)。即使不一一枚举出所有的最优前缀与最优后缀也不难看出这两个集合之间不会有任何的交集。因为任何的最有后缀都以”c”结尾,但没有任何最优前缀是以”c”结尾的,所以没有符合要求的,因此第七列赋值为0。
最后,让我们看看第八列的情况,也就是考虑到整个模板的时候(abababca)。不难看出最优前缀与最有后缀都以”a”开头以”a”结尾,所以第八列的值至少是1。而事实上1就是最终结果了,所有大于等于2的长度中,所有最有后缀都包含”c”,但只有”abababc”这一个最优前缀包含”c”,而七位的最优后缀”bababca”并不与其一致,所以第八列最终赋值为1。
如何使用部分匹配表
在已经匹配到部分字符后,如果发现下一个字符不再与模板匹配,此时通过使用部分匹配表就可以快速的跳过一些字符,从而避免冗余的比对提升效率。具体的使用方法可以用下面的话来解释:
如果已经匹配到的部分字符串的长度为partial_match_length,且字符串中下一个字符不再与模板匹配的情况下,如果table[partial_match_length] > 1那么我们可以跳过partial_match_length- table[partial_match_length – 1]这么多个字符。
还是拿”abababca”来举例,如果我们用这个模板来匹配” bacbababaabcbab”的话,我们的部分匹配表应该是这样的:
char: | a | b| a | b | a | b | c | a |
index: | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
value: | 0 | 0 | 1 | 2 | 3 | 4 | 0 | 1 |
第一次匹配的时候是这里:
ba cbababaabcbab
|
a bababca
此时partial_match_length值为1,对应的table[partial_match_length – 1] 即 table[0]为0,所以这种情况下我们不能跳过任何字符。下一次匹配的时候是这里:
bacb ababa abcbab
| | | | |
ababa bca
此时partial_match_length值为5,对应的table[partial_match_length – 1] 即 table[4]为3,这意味着我们可以跳过partial_match_length- table[partial_match_length – 1] ,即 5 – table[4] 即 5 – 3 亦即 2个字符。
// x 表示跳过一个字符
bacb ababa abcbab
xx | | |
aba babca
此时partial_match_length值为3,对应的table[partial_match_length- 1] 即 table[2]为1,这意味着我们可以跳过partial_match_length – table[partial_match_length – 1] ,即 3 – table[2]即 3 – 1 亦即 2个字符。
// x表示跳过一个字符
bacbababa abcbab
xx |
a bababca
此时此刻,模板长度大于所剩余的目标字符串长度,所以不可能会有匹配了。
结论
懂了没!正如我一开始所承诺的,没有纠结的解释也没有枯燥的证明。我就是这么理解的KMP。如果你有任何疑问或者发现我这篇文章哪里写错了,请给我留言,共同成长,与君共勉!