KMP算法

博客内容来自  http://www.ruanyifeng.com/blog/2013/05/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm.html

非常浅显易懂的kmp算法描述

字符串匹配的KMP算法

作者: 阮一峰

日期: 2013年5月 1日

字符串匹配是计算机的基本任务之一。

举例来说,有一个字符串"BBC ABCDAB ABCDABCDABDE",我想知道,里面是否包含另一个字符串"ABCDABD"?

许多算法可以完成这个任务,Knuth-Morris-Pratt算法(简称KMP)是最常用的之一。它以三个发明者命名,起头的那个K就是著名科学家Donald Knuth。

这种算法不太容易理解,网上有很多解释,但读起来都很费劲。直到读到Jake Boxer的文章,我才真正理解这种算法。下面,我用自己的语言,试图写一篇比较好懂的KMP算法解释。

1.

首先,字符串"BBC ABCDAB ABCDABCDABDE"的第一个字符与搜索词"ABCDABD"的第一个字符,进行比较。因为B与A不匹配,所以搜索词后移一位。

2.

因为B与A不匹配,搜索词再往后移。

3.

就这样,直到字符串有一个字符,与搜索词的第一个字符相同为止。

4.

接着比较字符串和搜索词的下一个字符,还是相同。

5.

直到字符串有一个字符,与搜索词对应的字符不相同为止。

6.

这时,最自然的反应是,将搜索词整个后移一位,再从头逐个比较。这样做虽然可行,但是效率很差,因为你要把"搜索位置"移到已经比较过的位置,重比一遍。

7.

一个基本事实是,当空格与D不匹配时,你其实知道前面六个字符是"ABCDAB"。KMP算法的想法是,设法利用这个已知信息,不要把"搜索位置"移回已经比较过的位置,继续把它向后移,这样就提高了效率。

8.

怎么做到这一点呢?可以针对搜索词,算出一张《部分匹配表》(Partial Match Table)。这张表是如何产生的,后面再介绍,这里只要会用就可以了。

9.

已知空格与D不匹配时,前面六个字符"ABCDAB"是匹配的。查表可知,最后一个匹配字符B对应的"部分匹配值"为2,因此按照下面的公式算出向后移动的位数:

  移动位数 = 已匹配的字符数 - 对应的部分匹配值

因为 6 - 2 等于4,所以将搜索词向后移动4位。

10.

因为空格与C不匹配,搜索词还要继续往后移。这时,已匹配的字符数为2("AB"),对应的"部分匹配值"为0。所以,移动位数 = 2 - 0,结果为 2,于是将搜索词向后移2位。

11.

因为空格与A不匹配,继续后移一位。

12.

逐位比较,直到发现C与D不匹配。于是,移动位数 = 6 - 2,继续将搜索词向后移动4位。

13.

逐位比较,直到搜索词的最后一位,发现完全匹配,于是搜索完成。如果还要继续搜索(即找出全部匹配),移动位数 = 7 - 0,再将搜索词向后移动7位,这里就不再重复了。

14.

下面介绍《部分匹配表》是如何产生的。

首先,要了解两个概念:"前缀"和"后缀"。 "前缀"指除了最后一个字符以外,一个字符串的全部头部组合;"后缀"指除了第一个字符以外,一个字符串的全部尾部组合。

15.

"部分匹配值"就是"前缀"和"后缀"的最长的共有元素的长度。以"ABCDABD"为例,

  - "A"的前缀和后缀都为空集,共有元素的长度为0;

  - "AB"的前缀为[A],后缀为[B],共有元素的长度为0;

  - "ABC"的前缀为[A, AB],后缀为[BC, C],共有元素的长度0;

  - "ABCD"的前缀为[A, AB, ABC],后缀为[BCD, CD, D],共有元素的长度为0;

  - "ABCDA"的前缀为[A, AB, ABC, ABCD],后缀为[BCDA, CDA, DA, A],共有元素为"A",长度为1;

  - "ABCDAB"的前缀为[A, AB, ABC, ABCD, ABCDA],后缀为[BCDAB, CDAB, DAB, AB, B],共有元素为"AB",长度为2;

  - "ABCDABD"的前缀为[A, AB, ABC, ABCD, ABCDA, ABCDAB],后缀为[BCDABD, CDABD, DABD, ABD, BD, D],共有元素的长度为0。

16.

"部分匹配"的实质是,有时候,字符串头部和尾部会有重复。比如,"ABCDAB"之中有两个"AB",那么它的"部分匹配值"就是2("AB"的长度)。搜索词移动的时候,第一个"AB"向后移动4位(字符串长度-部分匹配值),就可以来到第二个"AB"的位置。

(完)

珠峰培训

简寻

留言(109条)

回想起了高中NOIP的日子……

Robert Sedgewick https://class.coursera.org/algs4partII-001/lecture/40 用自动机解释kmp,感觉更妙,就是实现的时候空间复杂度高点。个人感觉Sedgewick这个老头讲东西确实牛逼,很多看书很难理解的算法,他一讲就明白了,有兴趣的可以去part1看看他讲的红黑树,特别牛逼。

有时候,字符串内部会有重复。比如,"ABCDAB"之中有两个"AB",那么它的"部分匹配值"就是2("AB"的长度)
这个应该说是,字符串头部和尾部有重复?
如果是ABCABD这样即使有两个AB也不能部分移动吧

的确简单易懂,可以来一本适用指南啊,想当初看这个算法 话了我好几天去理解 各种数学符号,各种证明 头都大了

引用test的发言:

这个应该说是,字符串头部和尾部有重复?
如果是ABCABD这样即使有两个AB也不能部分移动吧

谢谢指出,我确实没想到这一点,已经更正了。

那就是说需要搜索的词如果内部完全没有重复,那这个算法就退化成了遍历?

因为空格与A不匹配,继续后移一位。
但是这个移动位数 = 已匹配的字符数 - 对应的部分匹配值 = 0 - 0 = 0矛盾啊?

原来如此。解释得真清晰!

引用Chris的发言:

因为空格与A不匹配,继续后移一位。
但是这个移动位数 = 已匹配的字符数 - 对应的部分匹配值 = 0 - 0 = 0矛盾啊?

不是。如果第一个字符就不匹配,搜索词直接比较下一个字符,不用考虑《部分匹配表》。

表中第一列的那个“A”,是指有一个A匹配。

是我最喜欢的算法之一。

我一直以为KMP是string search最优的算法(时间复杂度是O(n+k)),直到我知道了Boyer–Moore算法……才发现原来还有更加巧妙的方法……

引用pi1ot的发言:

那就是说需要搜索的词如果内部完全没有重复,那这个算法就退化成了遍历?


没错,这个算法本质上就是在遍历基础上的一个改进。在最坏的情况下,和没有改进之前一样,甚至严格说还差了一点点,因为每一步都多了比较的开销。

引用sokoban的发言:


没错,这个算法本质上就是在遍历基础上的一个改进。在最坏的情况下,和没有改进之前一样,甚至严格说还差了一点点,因为每一步都多了比较的开销。

引用pi1ot的发言:

那就是说需要搜索的词如果内部完全没有重复,那这个算法就退化成了遍历?

应该不是的吧,最坏情况下也是o(m+n)的,而遍历是o(m*n)

引用Chris的发言:

应该不是的吧,最坏情况下也是o(m+n)的,而遍历是o(m*n)


你说得对。

但是最坏的个例的确可能一样。如在AAAAAAAAAAAAAAAAAAAAAAAAAA里面找AB

引用sokoban的发言:

你说得对。

但是最坏的个例的确可能一样。如在AAAAAAAAAAAAAAAAAAAAAAAAAA里面找AB

我还是错的。KMP的确似乎任何时候都要快一些。

极端例子:在AAAAAAAAAAAAAAAAAAAAAAAA里面找BB。

这个例子总算对了吧?

引用RedNax的发言:

我一直以为KMP是string search最优的算法(时间复杂度是O(n+k)),直到我知道了Boyer–Moore算法……才发现原来还有更加巧妙的方法……

搜索了一下,Boyer–Moore算法每次从要找的Pattern末尾开始比较,并且一般用两条规则来控制移位(KMP只有一条移位规则),比KMP还好。

引用sokoban的发言:

极端例子:在AAAAAAAAAAAAAAAAAAAAAAAA里面找BB。

这个例子总算对了吧?

不会
尝试匹配第一个,发现是A,所以要滑移。BB里面根本没有A,所以会滑移一个位置。

这是一个经典的势能分析例子。原串的指针只会往后移,目标串的指针只在原串指针往后移时往后移相同的步数,显然目标串指针往前移的步数不会多于往后移的步数。所以加起来所有指针移动的步数不会超过三倍的原串长度。

后缀匹配的平均时间可以是亚线性因为可以根据目标串的特点忽略掉原串的某些部分。但是最坏情况下还是线性的……

看《算法导论》的图32-10,就比较好理解了。

最简单解释就是把搜索词自相关一下

引用kanaz的发言:

最简单解释就是把搜索词自相关一下

能详细点么

我以前也写了一篇博文,理解kmp,核心在于next数组.我认为不要拘泥于过程,而要用抓住其递归的本质,摘录一部分如下:

理解next数组生成的算法花费了一些力气.该算法用了数学归纳法,读起来还有一些递归的意味.

这里再复述一下,求next[i+1],就是看P[1,i]的最大子串(姑且叫这个吧),如果匹配好说,如果不匹配则继续看这个最大子串的最大子串,重复这个过程,直到算出结果.

总结出了两种情况:
1:移动一位,如果搜索词第一个字符与目标字符串不匹配就移动一位,如果搜索词第一个字符与目标字符串匹配但是第二个字符与目标字符串不匹配也是移动一位(因为可以理解如果搜索词值匹配了第一个字符或者第一个字符都不匹配,那么展现不出特征无法推理搜索词移动一位是否是没有意义的,所以只能移动一位)
2:移动大于一位,如果搜索词第一个字符和第二个字符都与目标字符串匹配,那么就展现出了特征,可以推理,移动一位是没有意义的,所以可以尝试移动2位甚至更多
给博主一个建议:
觉得应该可以在部分匹配表再加一行移动位数的数据,以后直接查询移动位数就可以了,不要临时再算

各位有没有在实际的项目中用过KMP算法呢?

引用V客小站的发言:

各位有没有在实际的项目中用过KMP算法呢?

没有,我遇到过的实际需求更多是对一段长本文进行词表匹配或者替换,词表本身很大,但是每个词条比较短,类似于editor keyword highlight场景

楼主的这个算法感觉和这里面的例子第二步不能够对应上去
http://www.cs.utexas.edu/~moore/best-ideas/string-searching/kpm-example.html#step02

按理说
example的表应该是

0000001

ER和EX不匹配,按照楼主的说法应该是移动一位,但是例子里面直接移动了2位
应该是已经匹配的个数2减去EX对应的部分匹配值0,而不是1-0

@nklike:

是跟他不一样。

那里的理由是R不出现在EX里,所以可以移2位。我觉得那不是KMP算法,而是Boyer-Moore算法的“坏字符规则”。

最近时常从各个信息源获取到ruanyifeng的博客。小弟我也郑在自己学着积累思考,写博客。从博主这里学习了很多。

感谢LZ,这篇文章确实比较容易理解

有醍醐灌顶的感觉。

部分匹配值"就是"前缀"和"后缀"的最长的共有元素的长度,这里以AACDAA为例,

"AACDAA"的前缀为[A, AA, AAC, AACD, AACDA],后缀为[A,AA,DAA,CDAA,ACDAA],共有元素为"A","AA",这时取最长字符串的长度?

移动位数 = 已匹配的字符数 - 最后一个匹配字符对应的部分匹配值
这样更准确

@longsail:

最长字符串的长度是2。

初学KMP的时候,一直不得要领。后来学习AC自动机的时候,一下子明白了KMP实际上是AC自动机的特殊情况。

难道是我的错觉,第12步:

12.

逐位比较,直到发现C与D不匹配。于是,移动位数 = 6 - 2,继续将搜索词向后移动4位。

但是
搜索词 A B C D A B D
部分匹配值 0 0 0 0 1 2 0

移动位数 = 已匹配的字符数 - 对应的部分匹配值

难道不是应该 移动位数 = 6 - 0 吗? 最后一个D的匹配值是0啊。

Sorry, 我没有看到第16步。

Sorry, 看东西一直囫囵吞枣,仔细的看了几遍,又码了下代码,终于完全明白了。非常感谢。抱歉前面两次的留言。

非常不错,简介易懂,我转载了,并评价为:图文并茂,生动形象易懂。
http://50vip.com/blog.php?i=228

算法的关键在于next数组的生成,用动态规范法生成。
比如:str = abcabd,next初始化为:[0,0,0,0,0,0];
已知第0个字符a没有任何相同的前后缀,则next[0] = 0。
加入第1个字符,则前面已知的最长公共前后缀长度为next[0],此时如果str[next[0]]与str[1]相等,就可知道next[1]=next[0]+1,如不相等则可直接判定next[1]=0;这里str[next[0]]!=str[1],故next[1]=0。
...,next[2]=0。
...,next[3]=next[2]+1=1。
...,next[4]=next[3]+1=2。
...,next[5]=0。
最后,next=[0,0,0,1,2,0];

不知道KMP里面是否是这样生成的?

能把复杂的东西讲清楚,说明真的理解透彻了。看了这篇真的清晰很多,之前看《算法导论》,觉得看过就行了,其实是很多不懂,后来又忘了。

kmp算法主要是失效函数的这个表如何计算,其实质就是一个变形的状态机,用典型的数学归纳法来计算此表,有一个初始状态f(1)=0,然后迭代出所有位置上的失效值。
这里只说清楚了如何用失效函数来解决字符串匹配问题,但并没有给出如何计算失效函数的方法。

另外kmp算法对字母类语言理论效率比价高,但是对汉语这种没有太多前缀字串和后缀字串重合的语言,其实效率和最普通的匹配没太大差别。

省略了非常重要的一个问题,
就是部分匹配表的算法问题,如何得到?
只是给出原理是不够的。
另外,对于kmp算法正确性的解释,明显不够。
如果真心研究这个问题,还是需要去看看更想尽的解释,不要怕麻烦和困难。
看这个,只能了解表象。

@callmestring:

这个解释貌似不错。看next函数的生成代码,非常简洁,但是理解起来相当的不容易,不明白其推导过程。

如果是比较中文字符串怎么做呢?

@callmestring:

对于 str=[A,C,A,A]的str[3]就不正确了。

引用Mr.king的发言:

Robert Sedgewick https://class.coursera.org/algs4partII-001/lecture/40 用自动机解释kmp,感觉更妙,就是实现的时候空间复杂度高点。个人感觉Sedgewick这个老头讲东西确实牛逼,很多看书很难理解的算法,他一讲就明白了,有兴趣的可以去part1看看他讲的红黑树,特别牛逼。

Robert Sedgewick 这个实际上是string-matching automaton,需要预算这个pattern的整个字符集的自动机,个人感觉并不是kmp。

部分匹配值的概念为什么要提出来?为什么又要这么定义?抱歉看不大懂,不过对于KMP算法大概是明白了

引用radix的发言:

@callmestring:

对于 str=[A,C,A,A]的str[3]就不正确了。

就是,str = “acaa”,照这样算,在next[3]就不对了!!!

next值求得不对

解释的太清晰了,谢谢啦

按照你的做法
ababb 的next数组是什么呢?不对吧

做OJ题目的时候恰好用到,现在终于懂了KMP是怎么回事了~^ ^

为什么按照算法导论上的例子“ababababca”,再通过移动位数的公式得不到书上的那个数组呢?

感谢您的文章,真的写的很好,3Q!

看的第三篇关于KMP的,直接懂了,写得比其它两篇简单多了。
其实那两篇根本没认真看完,太长了,感觉对于有点计算机基础的人来说,KMP也不是一个需要那么多文字来描述的算法。
喜欢博主这种简单易懂的说明方式。

引用test的发言:

有时候,字符串内部会有重复。比如,"ABCDAB"之中有两个"AB",那么它的"部分匹配值"就是2("AB"的长度)
这个应该说是,字符串头部和尾部有重复?
如果是ABCABD这样即使有两个AB也不能部分移动吧

如果你匹配结束时,此时匹配的是ABCAB,此时查表肯定是2啊。
你仔细理解下后缀。后缀不是整个字符串的后缀,而是已经匹配部分的后缀。

最喜欢的博主的文章,通俗易懂,这才是真正的为人民服务。

解释的不错,终于看懂了,谢谢

部分匹配值都算出来了 为什么不算出移动值表呢

真的很清楚易懂啊,谢谢整理

我在网上看了好多,都不怎么懂,看了这个,恍然大悟。

之前看了很多博客都不理解,看了楼主的解释忽然间恍然大悟,谢谢楼主!

大神, 
我刚学三天的python.
碰巧看到了你的文章后,
我用三个多小时写出KMP啦!
会不会很蠢....
origin_str's length is: 23 match_pattern_str'length is: 7
------------origin_Str_index is: 0
------------origin_Str_index is: 1
------------origin_Str_index is: 2
------------origin_Str_index is: 3
------------origin_Str_index is: 4
length_matched is: 1
length_matched is: 2
length_matched is: 3
length_matched is: 4
length_matched is: 5
length_matched is: 6
now, pointer skips 4 index according to KMP
------------origin_Str_index is: 8
length_matched is: 1
length_matched is: 2
now, pointer skips 2 index according to KMP
------------origin_Str_index is: 10
------------origin_Str_index is: 11
length_matched is: 1
length_matched is: 2
length_matched is: 3
length_matched is: 4
length_matched is: 5
length_matched is: 6
now, pointer skips 4 index according to KMP
------------origin_Str_index is: 15
length_matched is: 1
length_matched is: 2
length_matched is: 3
length_matched is: 4
length_matched is: 5
length_matched is: 6
length_matched is: 7
position is at: 15
True

很有用,必须要留言,顶楼主!

非常好的文章,受益匪浅。请教一下:作者那个图是怎么画出来的?我指的是图1到16,尤其是上下对齐的红虚线框。谢谢。

引用HZ的发言:

非常好的文章,受益匪浅。请教一下:作者那个图是怎么画出来的?我指的是图1到16,尤其是上下对齐的红虚线框。谢谢。

可以在ppt、word里写好,然后截图吧,^_^

这篇看上去好懂多了,回头再结合其他的文章仔细看看。

有空的话把next数组的算法也讲讲吧

引用pi1ot的发言:

那就是说需要搜索的词如果内部完全没有重复,那这个算法就退化成了遍历?

完全没有重复也是O(n)的啊

寥寥数语,便将KMP算法的精髓尽数勾勒了出来,很好!

我用c++实现了下,这里贴不开,去我的博客看吧。
http://www.cnblogs.com/kaituorensheng/p/3633700.html

昨天开始研究这个算法一直不得要领,直到看到这篇博文。不能更赞。接下来结合其他书籍和相关资料深入理解,然后码码代码。

一峰的解释从来都是简明直白,能把复杂的东西清晰简洁的表述出来,而且还方便记忆。
KMP算法的前缀后缀的匹配理论真是优美。

引用test的发言:

有时候,字符串内部会有重复。比如,"ABCDAB"之中有两个"AB",那么它的"部分匹配值"就是2("AB"的长度)
这个应该说是,字符串头部和尾部有重复?
如果是ABCABD这样即使有两个AB也不能部分移动吧

你确定你说的对,按博主的理解是前缀是除字符串最后一个字符的所有组合,而后缀是除最前面一个字符的所有组合,如果是ABCABD,前缀的组合有{ABCAB,ABCA,ABC,AB,A},后缀有{BCABD,CABD,ABD,BD},这两个集合根本没有重复的,所有他的部分匹配值为0;

正在练习最简单的遍历匹配,直觉还有更高效的算法,这个虽然看不大懂,但是给了很大启发。

博主太nb了!KMP从头看下来,几分钟,就看懂了!

很受用,简单易懂,谢谢楼主分享。

如果按照这样的规则判断移动位数:移动位数 = 已匹配的字符数 - 对应的部分匹配值,
那第一位A如果不匹配应该如何计算移动位数呢?
移动位数 = 0(未匹配任何字符) - 0,结果还是0,这样的话第一位不匹配就不用移动了,显然是不对的。所以是不是把A的部分匹配值变为-1更合适?
谢谢

记得大学时,老师讲过,很麻烦,不过我想计算机执行起来是不会介意

膜拜楼主,楼主很厉害

第10步,后移4位之后,第一对儿AB和第二对AB重合,C对着一个空,那么这是我们还要从第一个AB开始比较么,比较和母串对AB相等,然后到C和空格不相等,是不是应该直接从C开始对比呢?

我用python实现了下,https://github.com/wkingfly/kmp 多多指教

好像有点漏洞:
比如:
ABCABDABCABC
ABCABC
因为C和D不匹配,按照文中的公式,应右移5-2=3位。其实可以直接移动6位。

其实理解kmp算法挺简单

只要先说出不适合使用这种算法的情况,大概读者就会明白了大概

模式字符串如果没有重复的字符,就不适合用kmp算法

next数组求起来还是有技巧的
按照阮一峰的解释会比较慢

谢谢楼主,这个真的是最容易理解的一个
如果您直接贴上kmp完整算法简直就是完美了

很感谢楼主,很快就懂了,还写好了代码,太开心了

请问这个部分匹配表和next[]数组是什么关系?

同问,博主说的这个部分匹配表和next数组有什么关系呢

看了《大话数据结构》,再看你的讲解,醍醐灌顶

看懂了一峰的这篇文章,等于是知道kmp是怎么操作的一个过程,所以我能马上写出一个初版的kmp算法。但这个初版的优化并不是很好,而且对于kmp为什么这么操作理解的并不是很透,所以后来又看了matrix67的那篇文章,终于懂了原理,然后根据他的伪代码写了最终版的kmp。
http://haihongblog.com/archives/911.html 这是我自己学习kmp的一个过程。
多谢一峰。

有个问题不明白,为什么"A"的前缀和后缀都为空集呢?我看《算法导论》上说,若w为x前缀,则有|w|

个人感觉鱼C工作室的KMP算法讲的很不错,可以去看看http://study.163.com/course/courseMain.htm?courseId=468002

这里的前后缀解释的很好,谢谢

看上去满复杂的算法,其实那句本质道出了原因,可以想到的是在搜索引擎里会有一定应用.

引用Ray的发言:

看上去满复杂的算法,其实那句本质道出了原因,可以想到的是在搜索引擎里会有一定应用.

发现错了,看了下篇算法立即就被打脸了,这个算法还是太简单

通俗易懂

阮老师讲的非常透彻,博客精湛程度堪比论文,佩服!!

感谢阮老师,本科看书没学明白的东西看明白了。

引用test的发言:

有时候,字符串内部会有重复。比如,"ABCDAB"之中有两个"AB",那么它的"部分匹配值"就是2("AB"的长度)
这个应该说是,字符串头部和尾部有重复?
如果是ABCABD这样即使有两个AB也不能部分移动吧

这个是什么意思?没理解,能帮忙解释一下吗?

引用阮一峰的发言:

谢谢指出,我确实没想到这一点,已经更正了。

你不需要更正,你的前缀与后缀的说明是对的,这种情况是不会有两个重复的AB的,看了你的讲解,很清晰易懂。

kMp算法模式匹配表的首位数字是-1 虽然思路和你差不多

找了好多资料,还是博主的这篇浅显易懂,不过要是能把next数组放出来就好了。。。

由“部分匹配值”表求next数组的方法十分十分简单:在“部分匹配值”表中,保持搜索词不动,将部分匹配值整体右一位(溢出的数不要了),然后初值赋为-1。

至于代码实现就不多说了。另外,Sunday算法比BM算法更快~
后来我在CDSN博主July的博文看到的,说下。最后谢谢一峰,看完这明白多了~真醍醐灌顶

讲的很棒,不过博主的网站有点粗糙哦,评论居然没有分页

很详细哈!!!

豁然开朗啊

为什么和CSDN的另一位都些相似,难道你们都借用了别人的东西。

比书上的写得太好了,如果有代码看就更好了

神作啊,课本看了好长时间都没看懂,这篇扫了一遍就懂了

我要发表看法

 «-必填

 «-必填,不公开

 «-我信任你,不会填写广告链接

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值