很详尽KMP算法(厉害)ZzUuOo666

本KMP原文最初写于2年多前的2011年12月,因当时初次接触KMP,思路混乱导致写也写得混乱。所以一直想找机会重新写下KMP,但苦于一直以来对KMP的理解始终不够,故才迟迟没有修改本文。

假设现在我们面临这样一个问题:有一个文本串S,和一个模式串P,现在要查找P在S中的位置,怎么查找呢?

如果用暴力匹配的思路,并假设现在文本串S匹配到i位置,模式串P匹配到j位置,则有:

举个例子,如果给定文本串S“BBCABCDABABCDABCDABDE”,和模式串P“ABCDABD”,现在要拿模式串P去跟文本串S匹配,整个过程如下所示:

1.S[0]为B,P[0]为A,不匹配,执行第②条指令:“如果失配(即S[i]!=P[j]),令i=i-(j-1),j=0”,S[1]跟P[0]匹配,相当于模式串要往右移动一位(i=1,j=0)

2.S[1]跟P[0]还是不匹配,继续执行第②条指令:“如果失配(即S[i]!=P[j]),令i=i-(j-1),j=0”,S[2]跟P[0]匹配(i=2,j=0),从而模式串不断的向右移动一位(不断的执行“令i=i-(j-1),j=0”,i从2变到4,j一直为0)

3.直到S[4]跟P[0]匹配成功(i=4,j=0),此时按照上面的暴力匹配算法的思路,转而执行第①条指令:“如果当前字符匹配成功(即S[i]==P[j]),则i++,j++”,可得S[i]为S[5],P[j]为P[1],即接下来S[5]跟P[1]匹配(i=5,j=1)

4.S[5]跟P[1]匹配成功,继续执行第①条指令:“如果当前字符匹配成功(即S[i]==P[j]),则i++,j++”,得到S[6]跟P[2]匹配(i=6,j=2),如此进行下去

5.直到S[10]为空格字符,P[6]为字符D(i=10,j=6),因为不匹配,重新执行第②条指令:“如果失配(即S[i]!=P[j]),令i=i-(j-1),j=0”,相当于S[5]跟P[0]匹配(i=5,j=0)

6.至此,我们可以看到,如果按照暴力匹配算法的思路,尽管之前文本串和模式串已经分别匹配到了S[9]、P[5],但因为S[10]跟P[6]不匹配,所以文本串回溯到S[5],模式串回溯到P[0],从而让S[5]跟P[0]匹配。

而S[5]肯定跟P[0]失配。为什么呢?因为在之前第4步匹配中,我们已经得知S[5]=P[1]=B,而P[0]=A,即P[1]!=P[0],故S[5]必定不等于P[0],所以回溯过去必然会导致失配。那有没有一种算法,让i不往回退,只需要移动j即可呢?

答案是肯定的。这种算法就是本文的主旨KMP算法,它利用之前已经部分匹配这个有效信息,保持i不回溯,通过修改j的位置,让模式串尽量地移动到有效的位置。

比如对于字符串aba来说,它有长度为1的相同前缀后缀a;而对于字符串abab来说,它有长度为2的相同前缀后缀ab(相同前缀后缀的长度为k+1,k+1=2)。

比如对于aba来说,第3个字符a之前的字符串ab中有长度为0的相同前缀后缀,所以第3个字符a对应的next值为0;而对于abab来说,第4个字符b之前的字符串aba中有长度为1的相同前缀后缀a,所以第4个字符b对应的next值为1(相同前缀后缀的长度为k,k=1)。

下面,咱们就结合之前的《最大长度表》和上述结论,进行字符串的匹配。如果给定文本串“BBCABCDABABCDABCDABDE”,和模式串“ABCDABD”,现在要拿模式串去跟文本串匹配,如下图所示:

通过上述匹配过程可以看出,问题的关键就是寻找模式串中最大长度的相同前缀和后缀,找到了模式串中每个字符之前的前缀和后缀公共部分的最大长度后,便可基于此匹配。而这个最大长度便正是next数组要表达的含义。

由上文,我们已经知道,字符串“ABCDABD”各个前缀后缀的最大公共元素长度分别为:

而且,根据这个表可以得出下述结论

把next数组跟之前求得的最大长度表对比后,不难发现,next数组相当于“最大长度值”整体向右移动一位,然后初始值赋为-1。意识到了这一点,你会惊呼原来next数组的求解竟然如此简单:就是找最大对称长度的前缀后缀,然后整体右移一位,初值赋为-1(当然,你也可以直接计算某个字符对应的next值,就是看这个字符之前的字符串中有多大长度的相同前缀后缀)。

换言之,对于给定的模式串:ABCDABD,它的最大长度表及next数组分别如下:

根据最大长度表求出了next数组后,从而有

而后,你会发现,无论是基于《最大长度表》的匹配,还是基于next数组的匹配,两者得出来的向右移动的位数是一样的。为什么呢?因为:

所以,你可以把《最大长度表》看做是next数组的雏形,甚至就把它当做next数组也是可以的,区别不过是怎么用的问题。

接下来,咱们来写代码求下next数组。

基于之前的理解,可知计算next数组的方法可以采用递推:

举个例子,如下图,根据模式串“ABCDABD”的next数组可知失配位置的字符D对应的next值为2,代表字符D前有长度为2的相同前缀和后缀(这个相同的前缀后缀即为“AB”),失配后,模式串需要向右移动j-next[j]=6-2=4位。

向右移动4位后,模式串中的字符C继续跟文本串匹配。

对于P的前j+1个序列字符:

用代码重新计算下“ABCDABD”的next数组,以验证之前通过“最长相同前缀后缀长度值右移一位,然后初值赋为-1”得到的next数组是否正确,计算结果如下表格所示:

从上述表格可以看出,无论是之前通过“最长相同前缀后缀长度值右移一位,然后初值赋为-1”得到的next数组,还是之后通过代码递推计算求得的next数组,结果是完全一致的。

下面,我们来基于next数组进行匹配。

还是给定文本串“BBCABCDABABCDABCDABDE”,和模式串“ABCDABD”,现在要拿模式串去跟文本串匹配,如下图所示:

在正式匹配之前,让我们来再次回顾下上文2.1节所述的KMP算法的匹配流程:

匹配过程一模一样。也从侧面佐证了,next数组确实是只要将各个最大前缀后缀的公共元素的长度值右移一位,且把初值赋为-1即可。

我们已经知道,利用next数组进行匹配失配时,模式串向右移动j-next[j]位,等价于已匹配字符数-失配字符的上一位字符所对应的最大长度值。原因是:

但为何本文不直接利用next数组进行匹配呢?因为next数组不好求,而一个字符串的前缀后缀的公共元素的最大长度值很容易求。例如若给定模式串“ababa”,要你快速口算出其next数组,乍一看,每次求对应字符的next值时,还得把该字符排除之外,然后看该字符之前的字符串中有最大长度为多大的相同前缀后缀,此过程不够直接。而如果让你求其前缀后缀公共元素的最大长度,则很容易直接得出结果:00123,如下表格所示:

然后这5个数字全部整体右移一位,且初值赋为-1,即得到其next数组:-10012。

next负责把模式串向前移动,且当第j位不匹配的时候,用第next[j]位和主串匹配,就像打了张“表”。此外,next也可以看作有限状态自动机的状态,在已经读了多少字符的情况下,失配后,前面读的若干个字符是有用的。

行文至此,咱们全面了解了暴力匹配的思路、KMP算法的原理、流程、流程之间的内在逻辑联系,以及next数组的简单求解(《最大长度表》整体右移一位,然后初值赋为-1)和代码求解,最后基于《next数组》的匹配,看似洋洋洒洒,清晰透彻,但以上忽略了一个小问题。

比如,如果用之前的next数组方法求模式串“abab”的next数组,可得其next数组为-1001(0012整体右移一位,初值赋为-1),当它跟下图中的文本串去匹配的时候,发现b跟c失配,于是模式串右移j-next[j]=3-1=2位。

右移2位后,b又跟c失配。事实上,因为在上一步的匹配中,已经得知p[3]=b,与s[3]=c失配,而右移两位之后,让p[next[3]]=p[1]=b再跟s[3]匹配时,必然失配。问题出在哪呢?

问题出在不该出现p[j]=p[next[j]]。为什么呢?理由是:当p[j]!=s[i]时,下次匹配必然是p[next[j]]跟s[i]匹配,如果p[j]=p[next[j]],必然导致后一步匹配失败(因为p[j]已经跟s[i]失配,然后你还用跟p[j]等同的值p[next[j]]去跟s[i]匹配,很显然,必然失配),所以不能允许p[j]=p[next[j]]。如果出现了p[j]=p[next[j]]咋办呢?如果出现了,则需要再次递归,即令next[j]=next[next[j]]。

所以,咱们得修改下求next数组的代码。

利用优化过后的next数组求法,可知模式串“abab”的新next数组为:-10-10。可能有些读者会问:原始next数组是前缀后缀最长公共元素长度值右移一位,然后初值赋为-1而得,那么优化后的next数组如何快速心算出呢?实际上,只要求出了原始next数组,便可以根据原始next数组快速求出优化后的next数组。还是以abab为例,如下表格所示:

只要出现了p[next[j]]=p[j]的情况,则把next[j]的值再次递归。例如在求模式串“abab”的第2个a的next值时,如果是未优化的next值的话,第2个a对应的next值为0,相当于第2个a失配时,下一步匹配模式串会用p[0]处的a再次跟文本串匹配,必然失配。所以求第2个a的next值时,需要再次递归:next[2]=next[next[2]]=next[0]=-1(此后,根据优化后的新next值可知,第2个a失配时,执行“如果j=-1,或者当前字符匹配成功(即S[i]==P[j]),都令i++,j++,继续匹配下一个字符”),同理,第2个b对应的next值为0。

对于优化后的next数组可以发现一点:如果模式串的后缀跟前缀相同,那么它们的next值也是相同的,例如模式串abcabc,它的前缀后缀都是abc,其优化后的next数组为:-100-100,前缀后缀abc的next值都为-100。

然后引用下之前3.1节的KMP代码:

接下来,咱们继续拿之前的例子说明,整个匹配过程如下:

1.S[3]与P[3]匹配失败。

2.S[3]保持不变,P的下一个匹配位置是P[next[3]],而next[3]=0,所以P[next[3]]=P[0]与S[3]匹配。

3.由于上一步骤中P[0]与S[3]还是不匹配。此时i=3,j=next[0]=-1,由于满足条件j==-1,所以执行“++i,++j”,即主串指针下移一个位置,P[0]与S[4]开始匹配。最后j==pLen,跳出循环,输出结果i-j=4(即模式串第一次在文本串中出现的位置),匹配成功,算法结束。

“KMP的算法流程:

BM算法定义了两个规则:

下面举例说明BM算法。例如,给定文本串“HEREISASIMPLEEXAMPLE”,和模式串“EXAMPLE”,现要查找模式串是否在文本串中,如果存在,返回模式串在文本串中的位置。

1.首先,"文本串"与"模式串"头部对齐,从尾部开始比较。"S"与"E"不匹配。这时,"S"就被称为"坏字符"(badcharacter),即不匹配的字符,它对应着模式串的第6位。且"S"不包含在模式串"EXAMPLE"之中(相当于最右出现位置是-1),这意味着可以把模式串后移6-(-1)=7位,从而直接移到"S"的后一位。

2.依然从尾部开始比较,发现"P"与"E"不匹配,所以"P"是"坏字符"。但是,"P"包含在模式串"EXAMPLE"之中。因为“P”这个“坏字符”对应着模式串的第6位(从0开始编号),且在模式串中的最右出现位置为4,所以,将模式串后移6-4=2位,两个"P"对齐。

3.依次比较,得到“MPLE”匹配,称为"好后缀"(goodsuffix),即所有尾部匹配的字符串。注意,"MPLE"、"PLE"、"LE"、"E"都是好后缀。

4.发现“I”与“A”不匹配:“I”是坏字符。如果是根据坏字符规则,此时模式串应该后移2-(-1)=3位。问题是,有没有更优的移法?

5.更优的移法是利用好后缀规则:当字符失配时,后移位数=好后缀在模式串中的位置-好后缀在模式串中上一次出现的位置,且如果好后缀在模式串中没有再次出现,则为-1。所有的“好后缀”(MPLE、PLE、LE、E)之中,只有“E”在“EXAMPLE”的头部出现,所以后移6-0=6位。可以看出,“坏字符规则”只能移3位,“好后缀规则”可以移6位。每次后移这两个规则之中的较大值。这两个规则的移动位数,只与模式串有关,与原文本串无关。

6.继续从尾部开始比较,“P”与“E”不匹配,因此“P”是“坏字符”,根据“坏字符规则”,后移6-4=2位。因为是最后一位就失配,尚未获得好后缀。

由上可知,BM算法不仅效率高,而且构思巧妙,容易理解。

Sunday算法由DanielM.Sunday在1990年提出,它的思想跟BM算法很相似:

下面举个例子说明下Sunday算法。假定现在要在文本串"substringsearchingalgorithm"中查找模式串"search"。

substringsearchingalgorithmsearch^3.结果第一个字符就不匹配,再看文本串中参加匹配的最末位字符的下一位字符,是'r',它出现在模式串中的倒数第3位,于是把模式串向右移动3位(r到模式串末尾的距离+1=2+1=3),使两个'r'对齐,如下:substringsearchingalgorithmsearch^

4.匹配成功。

回顾整个过程,我们只移动了两次模式串就找到了匹配位置,缘于Sunday算法每一步的移动量都比较大,效率很高。完。

THE END
1.来做选择题了视频 习近平出席庆祝澳门回归祖国25周年大会并发表重要讲话 美众议院投票通过应急拨款法案 数百游客参团后到机场才知没买机票 盛世莲花 在欢歌中精彩绽放 韦唯准备下山时曾遇车祸脊柱折断 直击哈尔滨冰雪大世界开园 热点问答|马来西亚何以原则同意重启马航370航班客机搜寻 海地宣布进入全国紧急状态 通过改革更好释放内需、https://m.163.com/v/video/VIIHQ7C74.html
2.KMP算法文章浏览阅读1.1w次,点赞3次,收藏9次。上:http://v.youku.com/v_show/id_XODYxNjExODQ=.html 第 34分钟开始 下:http://www.56.com/u28/v_NjAwMzA0ODA.html 严蔚敏_python kmp讲解视频https://blog.csdn.net/hnust_xiehonghao/article/details/7878329
3.kmp算法概念六、总结 KMP算法是一种高效的字符串匹配算法,它通过预处理模式串,在匹配过程中避免重复比较已经比较过的字符,从而提高了匹配效率。其核心概念是next数组,通过求解next数组可以实现快速跳过已经比较过的位置。KMP算法具有广泛的应用场景,在搜索引擎、音频和视频处理等领域都有着重要作用。?https://wenku.baidu.com/view/4b6f43810a75f46527d3240c844769eae109a339.html
4.KMP算法引入:如何设计一个算法,判断一个字符串中含有另一个字符串呢?比较容易想到的就是朴素模式匹配算法,时间复杂度是O(n*m),但是kmp算法可以让时间复杂度降低到O(n)。 这是如何做到的? 假设有两个字符串为 A=ababbababa B=ababa 不妨令A为主串,B为模式串,可以发现,A[5]和B[5](不妨设下标从1开始)失配了,https://www.jianshu.com/p/939815304ff6
5.如何更好地理解和掌握KMP算法?KMP算法 内部涉及到的数学原理与知识太多,本文只会对 KMP算法 的运行过程、 部分匹配表、next数组 进行介绍,如果理解了这三点再去阅读其它有关 KMP算法 的文章肯定能有个清晰的认识。 以下的文字描述请结合视频动画来阅读~ 0 定义 Knuth-Morris-Pratt 字符串查找算法,简称为 KMP算法,常用于在一个文本串 S 内https://www.zhihu.com/question/21923021
6.彻底搞懂KMP算法原理腾讯云开发者社区KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人们称它为克努特—莫里斯—普拉特操作(简称KMP算法)。KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是通过一个next()函数实现,函数本身包含了模式串的局部匹配信息。KMPhttps://cloud.tencent.com/developer/article/2235837
7.数据结构与算法视频下载北京大学张铭老师视频下载第八讲 第3章 字符串——2(模式匹配、KMP算法) http://db.pku.edu.cn/mzhang/ds/media/8_String_KMP.rm 第九讲 第4章 二叉树——1(二叉树的概念和ADT) http://db.pku.edu.cn/mzhang/ds/media/9_BT_ADT.rm 第十讲 第4章 二叉树——2(二叉树的周游) http://www.blogjava.net/jjshcc/archive/2012/05/29/379487.html
8.KMP最经典版本2.9.3.1428版KMP.rar_https.//kmp80.com 关于KMP算法:字符串匹配问题。朴素匹配算法的改进版本。理解起来有些难度,但是十分高效 上传者:weixin_42662293时间:2022-09-23 KMPlayer 2.9.3.1428 来自韩国的影音全能播放器,与Mplayer一样从linux平台移植而来的Kmplayer(简称KMP)几乎可以播放您系统上所有的影音文件。通过各种插件扩展KMPhttps://www.iteye.com/resource/songstrong-4258449
9.浅谈Python描述数据结构之KMP篇pythonKMP算法,是由D.E.Knuth、J.H.Morris、V.R.PrattD.E.Knuth、J.H.Morris、V.R.PrattD.E.Knuth、J.H.Morris、V.R.Pratt同时发现的,又被称为克努特-莫里斯-普拉特算法。该算法的基本思路就是在匹配失败后,无需回到主串和模式串最近一次开始比较的位置,而是在不改变主串已经匹配到的位置的前提下,根https://www.jb51.net/article/195018.htm
10.算法图文动画详解系列KMP字符串查找算法(KnuthMorrisKMP 算法核心原理示意图 KMP算法原理详解视频: 算法图文详解: 求解前缀表 next[] 的核心思想 把前缀 P[0:j]? 当成是 P 的模式串(P[0:i] ),P 本身当成是查找的文本。 next [] : 前缀表数组,上图中是 lps 数组。 https://blog.51cto.com/u_15236724/5365994
11.刷题攻略:200道经典题目刷题顺序,共60w字的详细图解,视频难点帮你把KMP算法学个通透 双指针法 双指针法基本都是应用在数组,字符串与链表的题目上 栈与队列:逆波兰表达式求值 二叉树 题目分类大纲如下: 关于二叉树,你该了解这些! 二叉树:二叉树的递归遍历 二叉树:二叉树的迭代遍历 二叉树:二叉树的统一迭代法 https://github.com/gelansheshed/leetcode-master
12.KMP算法最简单的做法是两个循环分别比较每个字符,直到找到匹配的位置或遍历结束。这种做法需要消耗 O(m * n) (m 表示主字符串的长度,n 表示模式串的长度) 的时间复杂度。而 KMP 算法则可以通过跳过一些重复的比较过程将时间复杂度控制在 O(m + n) (m 表示主字符串的长度,n 表示模式串的长度)。https://www.imooc.com/article/295341
13.《算法(第4版)》KMP理解(算法(第4版))书评KMP算法“KMP算法的主要思想是提前判断如何重新开始查找,而这种判断只取决于模式字符串本身。”——摘自《算法》中文版496页如果说这本书中介绍的关于实现KMP算法的方法和其它文章中构建next数组的方法有什么不同的话,我认为是前者不仅不回退,而且一直在”前行”,而后者的i不回退但是会“停留”。不过这篇文章只单独https://book.douban.com/review/10238312/
14.动画:七分钟理解什么是KMP算法吴师兄学算法本文是介绍什么是BF算法、KMP算法、BM算法三部曲之一。 KMP算法内部涉及到的数学原理与知识太多,本文只会对KMP算法的运行过程、部分匹配表、next数组进行介绍,如果理解了这三点再去阅读其它有关KMP算法的文章肯定能有个清晰的认识。 以下的文字描述请结合视频动画来阅读~ https://www.cxyxiaowu.com/549.html
15.字符串匹配算法(BFKMP)BF算法思路简单。但当匹配失败时,主串的指针i总是回溯到i-j+1位置,模式串的指针j总是恢复到首字符位置j=1。因此,算法时间复杂度高。 三、KMP算法 (以下分析元素下标皆从1开始) 改进点:失配时,不需回溯i指针,而是利用已经比较过的信息,仅将模式串向右移动到一个合适的位置,并从这个位置开始和主串进行比较,https://m.nowcoder.com/discuss/512039365049069568
16.KMP模式匹配算法知识点无回溯的模式匹配中最具代表性的是KMP算法。它是基于对模式本身的字符分布特征所进行的分析,生成模式的特征向量,并在模式匹配的过程中对此加以利用,以提高模式匹配的效率,其时间代价是目标串长度的线性函数,同时模式的特征向量的计算也与模式本身长度成正比。在KMP算法中最关键的部分是模式的特征向量的计算和生成。KMPhttp://jpk.pku.edu.cn/course/sjjg/chapter3/04/03.html
17.数据结构(下)清华大学F3. 左式堆:合并算法 F4. 左式堆:插入 + 删除 本章测验 6第十三章 串 A. ADT B. 模式匹配 C1. KMP算法:记忆法 C2. KMP算法:查询表 C3. KMP算法:理解next[]表 C4. KMP算法:构造next[]表 C5. KMP算法:分摊分析 C6. KMP算法:再改进 https://www.xuetangx.com/courses/course-v1:TsinghuaX+30240184+2015_T2/bb8b95e3c75243e7aedde08095c0f225