1、KMP算法之浅析KMP算法之浅析1. 引言 本KMP原文最初写于2年多前的2011年12月,因当时初次接触KMP,思路混乱导致写也写得混乱,如此,留言也是“骂声”一片。所以一直想找机会重新写下KMP,但苦于一直以来对KMP的理解始终不够,故才迟迟没有修改本文。 然近期因在北京开了个算法班,专门讲解数据结构、面试、算法,才再次仔细回顾了这个KMP,在综合了一些网友的理解、以及跟我一起讲算法的两位讲师朋友曹博、邹博的理解之后,写了9张PPT,发在微博上。随后,一不做二不休,索性将PPT上的内容整理到了本文之中(后来文章越写越完整,所含内容早已不再是九张PPT 那样简单了)。 KMP本身不复杂,但网
2、上绝大部分的文章(包括本文的2011年版本)把它讲混乱了。下面,咱们从暴力匹配算法讲起,随后阐述KMP的流程 步骤、next 数组的简单求解 递推原理 代码求解,接着基于next 数组匹配,谈到有限状态自动机,next 数组的优化,KMP的时间复杂度分析,最后简要介绍两个KMP的扩展算法。 全文力图给你一个最为完整最为清晰的KMP,希望更多的人不再被KMP折磨或纠缠,不再被一些混乱的文章所混乱,有何疑问,欢迎随时留言评论,thanks。2. 暴力匹配算法 假设现在我们面临这样一个问题:有一个文本串S,和一个模式串P,现在要查找P在S中的位置,怎么查找呢? 如果用暴力匹配的思路,并假设现在文本串
3、S匹配到 i 位置,模式串P匹配到 j 位置,则有: 如果当前字符匹配成功(即Si = Pj),则i+,j+,继续匹配下一个字符; 如果失配(即Si! = Pj),令i = i - (j - 1),j = 0。相当于每次匹配失败时,i 回溯,j 被置为0。 理清楚了暴力匹配算法的流程及内在的逻辑,咱们可以写出暴力匹配的代码,如下:1. intViolentMatch(char*s,char*p)2. 3. intsLen=strlen(s);4. intpLen=strlen(p);5. 6. inti=0;7. intj=0;8. while(isLen&j 0,代表下次匹配跳到j 之前的某
4、个字符,而不是跳到开头,且具体跳过了k 个字符。 转换成代码表示,则是:1. intKmpSearch(char*s,char*p)2. 3. inti=0;4. intj=0;5. intsLen=strlen(s);6. intpLen=strlen(p);7. while(isLen&jpLen)8. 9. /如果j=-1,或者当前字符匹配成功(即Si=Pj),都令i+,j+10. if(j=-1|si=pj)11. 12. i+;13. j+;14. 15. else16. 17. /如果j!=-1,且当前字符匹配失败(即Si!=Pj),则令i不变,j=nextj18. /nextj即
5、为j所对应的next值19. j=nextj;20. 21. 22. if(j=pLen)23. returni-j;24. else25. return-1;26. 继续拿之前的例子来说,当S10跟P6匹配失败时,KMP不是跟暴力匹配那样简单的把模式串右移一位,而是执行第条指令:“如果j != -1,且当前字符匹配失败(即Si != Pj),则令 i 不变,j = nextj”,即j 从6变到2(后面我们将求得P6,即字符D对应的next 值为2),所以相当于模式串向右移动的位数为j - nextj(j - nextj =6-2 = 4)。 向右移动4位后,S10跟P2继续匹配。为什么要向右
6、移动4位呢,因为移动4位后,模式串中又有个“AB”可以继续跟S8S9对应着,从而不用让i 回溯。相当于在除去字符D的模式串子串中寻找相同的前缀和后缀,然后根据前缀后缀求出next 数组,最后基于next 数组进行匹配(不关心next 数组是怎么求来的,只想看匹配过程是咋样的,可直接跳到下文3.3.4节)。3.2 步骤 寻找前缀后缀最长公共元素长度o 对于P = p0 p1 .pj-1 pj,寻找模式串P中长度最大且相等的前缀和后缀。如果存在p0 p1 .pk-1 pk = pj- k pj-k+1.pj-1 pj,那么在包含pj的模式串中有最大长度为k+1的相同前缀后缀。举个例子,如果给定的模
7、式串为“abab”,那么它的各个子串的前缀后缀的公共元素的最大长度如下表格所示:比如对于字符串aba来说,它有长度为1的相同前缀后缀a;而对于字符串abab来说,它有长度为2的相同前缀后缀ab(相同前缀后缀的长度为k + 1,k + 1 = 2)。 求next数组o next 数组考虑的是除当前字符外的最长相同前缀后缀,所以通过第步骤求得各个前缀后缀的公共元素的最大长度后,只要稍作变形即可:将第步骤中求得的值整体右移一位,然后初值赋为-1,如下表格所示:比如对于aba来说,第3个字符a之前的字符串ab中有长度为0的相同前缀后缀,所以第3个字符a对应的next值为0;而对于abab来说,第4个字
8、符b之前的字符串aba中有长度为1的相同前缀后缀a,所以第4个字符b对应的next值为1(相同前缀后缀的长度为k,k = 1)。 根据next数组进行匹配o 匹配失配,j = next j,模式串向右移动的位数为:j - nextj。换言之,当模式串的后缀pj-k pj-k+1, ., pj-1 跟文本串si-k si-k+1, ., si-1匹配成功,但pj 跟si匹配失败时,因为nextj = k,相当于在不包含pj的模式串中有最大长度为k 的相同前缀后缀,即p0 p1 .pk-1 = pj-k pj-k+1.pj-1,故令j = nextj,从而让模式串右移j - nextj 位,使得模
9、式串的前缀p0 p1, ., pk-1对应着文本串 si-k si-k+1, ., si-1,而后让pk 跟si 继续匹配。如下图所示: 综上,KMP的next 数组相当于告诉我们:当模式串中的某个字符跟文本串中的某个字符匹配失配时,模式串下一步应该跳到哪个位置。如模式串中在j 处的字符跟文本串在i 处的字符匹配失配时,下一步用next j 处的字符继续跟文本串i 处的字符匹配,相当于模式串向右移动 j - nextj 位。 接下来,分别具体解释上述3个步骤。3.3 解释3.3.1 寻找最长前缀后缀 如果给定的模式串是:“ABCDABD”,从左至右遍历整个模式串,其各个子串的前缀后缀分别如下表
10、格所示: 也就是说,原模式串子串对应的各个前缀后缀的公共元素的最大长度表为(下简称最大长度表):3.3.2 基于最大长度表匹配 因为模式串中首尾可能会有重复的字符,故可得出下述结论:失配时,模式串向右移动的位数为:已匹配字符数 - 失配字符的上一位字符所对应的最大长度值 下面,咱们就结合之前的最大长度表和上述结论,进行字符串的匹配。如果给定文本串“BBC ABCDAB ABCDABCDABDE”,和模式串“ABCDABD”,现在要拿模式串去跟文本串匹配,如下图所示: 1. 因为模式串中的字符A跟文本串中的字符B、B、C、空格一开始就不匹配,所以不必考虑结论,直接将模式串不断的右移一位即可,直到
11、模式串中的字符A跟文本串的第5个字符A匹配成功: 2.继续往后匹配,当模式串最后一个字符D跟文本串匹配时失配,显而易见,模式串需要向右移动。但向右移动多少位呢?因为此时已经匹配的字符数为6个(ABCDAB),然后根据最大长度表可得失配字符D的上一位字符B对应的长度值为2,所以根据之前的结论,可知需要向右移动6 - 2 = 4 位。 3. 模式串向右移动4位后,发现C处再度失配,因为此时已经匹配了2个字符(AB),且上一位字符B对应的最大长度值为0,所以向右移动:2 - 0 =2 位。 4. A与空格失配,向右移动1 位。 5. 继续比较,发现D与C 失配,故向右移动的位数为:已匹配的字符数6减
12、去上一位字符B对应的最大长度2,即向右移动6 - 2 = 4 位。 6. 经历第5步后,发现匹配成功,过程结束。 通过上述匹配过程可以看出,问题的关键就是寻找模式串中最大长度的相同前缀和后缀,找到了模式串中每个字符之前的前缀和后缀公共部分的最大长度后,便可基于此匹配。而这个最大长度便正是next 数组要表达的含义。3.3.3 根据最大长度表求next 数组 由上文,我们已经知道,字符串“ABCDABD”各个前缀后缀的最大公共元素长度分别为: 而且,根据这个表可以得出下述结论 失配时,模式串向右移动的位数为:已匹配字符数- 失配字符的上一位字符所对应的最大长度值 上文利用这个表和结论进行匹配时,
13、我们发现,当匹配到一个字符失配时,其实没必要考虑当前失配的字符,更何况我们每次失配时,都是看的失配字符的上一位字符对应的最大长度值。如此,便引出了next 数组。 给定字符串“ABCDABD”,可求得它的next 数组如下: 把next 数组跟之前求得的最大长度表对比后,不难发现,next 数组相当于“最大长度值” 整体向右移动一位,然后初始值赋为-1。意识到了这一点,你会惊呼原来next 数组的求解竟然如此简单:就是找最大对称长度的前缀后缀,然后整体右移一位,初值赋为-1(当然,你也可以直接计算某个字符对应的next值,就是看这个字符之前的字符串中有多大长度的相同前缀后缀)。 换言之,对于给
14、定的模式串:ABCDABD,它的最大长度表及next 数组分别如下: 根据最大长度表求出了next 数组后,从而有失配时,模式串向右移动的位数为:失配字符所在位置- 失配字符对应的next 值 而后,你会发现,无论是基于最大长度表的匹配,还是基于next 数组的匹配,两者得出来的向右移动的位数是一样的。为什么呢?因为: 根据最大长度表,失配时,模式串向右移动的位数 = 已经匹配的字符数 - 失配字符的上一位字符的最大长度值 而根据next 数组,失配时,模式串向右移动的位数 = 失配字符的位置 - 失配字符对应的next 值o 其中,从0开始计数时,失配字符的位置 = 已经匹配的字符数(失配字
15、符不计数),而失配字符对应的next 值 =失配字符的上一位字符的最大长度值,两相比较,结果必然完全一致。 所以,你可以把最大长度表看做是next 数组的雏形,甚至就把它当做next 数组也是可以的,区别不过是怎么用的问题。3.3.4 通过代码递推计算next 数组 接下来,咱们来写代码求下next 数组。 基于之前的理解,可知计算next 数组的方法可以采用递推: 1. 如果对于值k,已有p0 p1, ., pk-1 = pj-k pj-k+1, ., pj-1,相当于nextj = k。o 此意味着什么呢?究其本质,nextj = k 代表pj 之前的模式串子串中,有长度为k 的相同前缀和
16、后缀。有了这个next 数组,在KMP匹配中,当模式串中j 处的字符失配时,下一步用nextj处的字符继续跟文本串匹配,相当于模式串向右移动j - nextj 位。举个例子,如下图,根据模式串“ABCDABD”的next 数组可知失配位置的字符D对应的next 值为2,代表字符D前有长度为2的相同前缀和后缀(这个相同的前缀后缀即为“AB”),失配后,模式串需要向右移动j - next j = 6 - 2 =4位。向右移动4位后,模式串中的字符C继续跟文本串匹配。 2. 下面的问题是:已知next 0, ., j,如何求出next j + 1呢? 对于P的前j+1个序列字符: 若pk = pj,
17、则nextj + 1 = next j + 1 = k + 1; 若pk pj,如果此时pnextk = pj ,则next j + 1 = nextk+ 1,否则继续递归前缀索引k = nextk,而后重复此过程。相当于在字符pj+1之前不存在长度为k+1的前缀p0 p1, , pk-1 pk跟后缀“pj-k pj-k+1, , pj-1 pj相等,那么是否可能存在另一个值t+1 k+1,使得长度更小的前缀 “p0 p1, , pt-1 pt” 等于长度更小的后缀 “pj-t pj-t+1, , pj-1 pj” 呢?如果存在,那么这个t+1 便是next j+1的值,此相当于利用已经求得的
18、next 数组(next 0, ., k, ., j)进行P串前缀跟P串后缀的匹配。 一般的文章或教材可能就此一笔带过,但大部分的初学者可能还是不能很好的理解上述求解next 数组的原理,故接下来,我再来着重说明下。 如下图所示,假定给定模式串ABCDABCE,且已知next j = k(相当于“p0 pk-1” = “pj-k pj-1” = AB,可以看出k为2),现要求next j + 1等于多少?因为pk = pj = C,所以nextj + 1 = nextj + 1 = k + 1(可以看出nextj + 1 = 3)。代表字符E前的模式串中,有长度k+1 的相同前缀后缀。 但如果
19、pk != pj 呢?说明“p0 pk-1 pk” “pj-k pj-1 pj”。换言之,当pk != pj后,字符E前有多大长度的相同前缀后缀呢?很明显,因为C不同于D,所以ABC 跟 ABD不相同,即字符E前的模式串没有长度为k+1的相同前缀后缀,也就不能再简单的令:nextj + 1 = nextj + 1 。所以,咱们只能去寻找长度更短一点的相同前缀后缀。 结合上图来讲,若能在前缀“ p0 pk-1 pk ” 中不断的递归前缀索引k = next k,找到一个字符pk 也为D,代表pk = pj,且满足p0 pk-1 pk = pj-k pj-1 pj,则最大相同的前缀后缀长度为k +
20、 1,从而next j + 1 = k + 1 = next k + 1。否则前缀中没有D,则代表没有相同的前缀后缀,next j + 1 = 0。 那为何递归前缀索引k = nextk,就能找到长度更小的相同前缀后缀呢?这又归根到next数组的含义。为了寻找长度相同的前缀后缀,我们拿前缀 p0 pk-1 pk 去跟后缀pj-k pj-1 pj匹配,如果pk 跟pj 失配,下一步就是用pnextk 去跟pj 继续匹配,如果p nextk 跟pj还是不匹配,则下一步用p next nextk 去跟pj匹配。相当于在不断的递归k = nextk,直到要么找到长度更小的相同前缀后缀,要么没有长度更小
21、的相同前缀后缀。 所以,因最终在前缀ABC中没有找到D,故E的next 值为0:模式串的后缀:ABDE模式串的前缀:ABC前缀右移两位: ABC 读到此,有的读者可能又有疑问了,那能否举一个能在前缀中找到字符D的例子呢?OK,咱们便来看一个能在前缀中找到字符D的例子,如下图所示: 给定模式串DABCDABDE,我们很顺利的求得字符D之前的“DABCDAB”的各个子串的最长相同前缀后缀的长度分别为0 0 0 0 1 2 3,但当遍历到字符D,要求包括D在内的“DABCDABD”最长相同前缀后缀时,我们发现pj处的字符D跟pk处的字符C不一样,换言之,前缀DABC的最后一个字符C 跟后缀DABD的最后一个字符D不相同,所以不存在长度为4的相同前缀后缀。 怎么办呢?既然没有长度为4的相同前缀后缀,咱们可以寻找长度短点的相同前缀后缀,最终,因在p0处发现也有个字符D,p0 = pj,所以pj对应的长度值为1,相当于E对应的next 值为1。 综上,可以通过递推求得next 数组,代码如下所示:1. voidGetNext(char*p,intnext)2. 3. intpLen=strlen(p);4. next
copyright@ 2008-2022 冰豆网网站版权所有
经营许可证编号:鄂ICP备2022015515号-1