KMP算法之浅析.docx

上传人:b****3 文档编号:12673558 上传时间:2023-04-21 格式:DOCX 页数:34 大小:411KB
下载 相关 举报
KMP算法之浅析.docx_第1页
第1页 / 共34页
KMP算法之浅析.docx_第2页
第2页 / 共34页
KMP算法之浅析.docx_第3页
第3页 / 共34页
KMP算法之浅析.docx_第4页
第4页 / 共34页
KMP算法之浅析.docx_第5页
第5页 / 共34页
点击查看更多>>
下载资源
资源描述

KMP算法之浅析.docx

《KMP算法之浅析.docx》由会员分享,可在线阅读,更多相关《KMP算法之浅析.docx(34页珍藏版)》请在冰豆网上搜索。

KMP算法之浅析.docx

KMP算法之浅析

KMP算法之浅析

1.引言

  本KMP原文最初写于2年多前的2011年12月,因当时初次接触KMP,思路混乱导致写也写得混乱,如此,留言也是“骂声”一片。

所以一直想找机会重新写下KMP,但苦于一直以来对KMP的理解始终不够,故才迟迟没有修改本文。

  然近期因在北京开了个算法班,专门讲解数据结构、面试、算法,才再次仔细回顾了这个KMP,在综合了一些网友的理解、以及跟我一起讲算法的两位讲师朋友曹博、邹博的理解之后,写了9张PPT,发在微博上。

随后,一不做二不休,索性将PPT上的内容整理到了本文之中(后来文章越写越完整,所含内容早已不再是九张PPT那样简单了)。

  KMP本身不复杂,但网上绝大部分的文章(包括本文的2011年版本)把它讲混乱了。

下面,咱们从暴力匹配算法讲起,随后阐述KMP的流程步骤、next数组的简单求解递推原理代码求解,接着基于next数组匹配,谈到有限状态自动机,next数组的优化,KMP的时间复杂度分析,最后简要介绍两个KMP的扩展算法。

  全文力图给你一个最为完整最为清晰的KMP,希望更多的人不再被KMP折磨或纠缠,不再被一些混乱的文章所混乱,有何疑问,欢迎随时留言评论,thanks。

2.暴力匹配算法

  假设现在我们面临这样一个问题:

有一个文本串S,和一个模式串P,现在要查找P在S中的位置,怎么查找呢?

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

∙如果当前字符匹配成功(即S[i]==P[j]),则i++,j++,继续匹配下一个字符;

∙如果失配(即S[i]!

=P[j]),令i=i-(j-1),j=0。

相当于每次匹配失败时,i回溯,j被置为0。

  理清楚了暴力匹配算法的流程及内在的逻辑,咱们可以写出暴力匹配的代码,如下:

1.int ViolentMatch(char* s, char* p)  

2.{  

3.    int sLen = strlen(s);  

4.    int pLen = strlen(p);  

5.  

6.    int i = 0;  

7.    int j = 0;  

8.    while (i < sLen && j < pLen)  

9.    {  

10.        if (s[i] == p[j])  

11.        {  

12.            //①如果当前字符匹配成功(即S[i] == P[j]),则i++,j++      

13.            i++;  

14.            j++;  

15.        }  

16.        else  

17.        {  

18.            //②如果失配(即S[i]!

 = P[j]),令i = i - (j - 1),j = 0      

19.            i = i - j + 1;  

20.            j = 0;  

21.        }  

22.    }  

23.    //匹配成功,返回模式串p在文本串s中的位置,否则返回-1  

24.    if (j == pLen)  

25.        return i - j;  

26.    else  

27.        return -1;  

28.}  

  举个例子,如果给定文本串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的位置,让模式串尽量地移动到有效的位置。

3.KMP算法

3.1定义

   Knuth-Morris-Pratt字符串查找算法,简称为“KMP算法”,常用于在一个文本串S内查找一个模式串P的出现位置,这个算法由DonaldKnuth、VaughanPratt、JamesH.Morris三人于1977年联合发表,故取这3人的姓氏命名此算法。

  下面先直接给出KMP的算法流程(如果感到一点点不适,没关系,坚持下,稍后会有具体步骤及解释,越往后看越会柳暗花明☺):

∙假设现在文本串S匹配到i位置,模式串P匹配到j位置

o如果j=-1,或者当前字符匹配成功(即S[i]==P[j]),都令i++,j++,继续匹配下一个字符;

o如果j!

=-1,且当前字符匹配失败(即S[i]!

=P[j]),则令i不变,j=next[j]。

此举意味着失配时,模式串P相对于文本串S向右移动了j-next[j]位。

▪换言之,当匹配失败时,模式串向右移动的位数为:

失配字符所在位置-失配字符对应的next值(next数组的求解会在下文的3.3.3节中详细阐述),即移动的实际位数为:

j-next[j],且此值大于等于1。

  很快,你也会意识到next数组各值的含义:

代表当前字符之前的字符串中,有多大长度的相同前缀后缀。

例如如果next[j]=k,代表j之前的字符串中有最大长度为k 的相同前缀后缀。

  此也意味着在某个字符失配时,该字符对应的next值会告诉你下一步匹配中,模式串应该跳到哪个位置(跳到next[j]的位置)。

如果next[j]等于0或-1,则跳到模式串的开头字符,若next[j]=k且k>0,代表下次匹配跳到j之前的某个字符,而不是跳到开头,且具体跳过了k个字符。

  转换成代码表示,则是:

1.int KmpSearch(char* s, char* p)  

2.{  

3.    int i = 0;  

4.    int j = 0;  

5.    int sLen = strlen(s);  

6.    int pLen = strlen(p);  

7.    while (i < sLen && j < pLen)  

8.    {  

9.        //①如果j = -1,或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++      

10.        if (j == -1 || s[i] == p[j])  

11.        {  

12.            i++;  

13.            j++;  

14.        }  

15.        else  

16.        {  

17.            //②如果j !

= -1,且当前字符匹配失败(即S[i] !

= P[j]),则令 i 不变,j = next[j]      

18.            //next[j]即为j所对应的next值        

19.            j = next[j];  

20.        }  

21.    }  

22.    if (j == pLen)  

23.        return i - j;  

24.    else  

25.        return -1;  

26.}  

  继续拿之前的例子来说,当S[10]跟P[6]匹配失败时,KMP不是跟暴力匹配那样简单的把模式串右移一位,而是执行第②条指令:

“如果j!

=-1,且当前字符匹配失败(即S[i]!

=P[j]),则令i不变,j=next[j]”,即j从6变到2(后面我们将求得P[6],即字符D对应的next值为2),所以相当于模式串向右移动的位数为j-next[j](j-next[j]= 6-2=4)。

  向右移动4位后,S[10]跟P[2]继续匹配。

为什么要向右移动4位呢,因为移动4位后,模式串中又有个“AB”可以继续跟S[8]S[9]对应着,从而不用让i回溯。

相当于在除去字符D的模式串子串中寻找相同的前缀和后缀,然后根据前缀后缀求出next数组,最后基于next数组进行匹配(不关心next数组是怎么求来的,只想看匹配过程是咋样的,可直接跳到下文3.3.4节)。

3.2步骤

∙①寻找前缀后缀最长公共元素长度

o对于P=p0p1...pj-1pj,寻找模式串P中长度最大且相等的前缀和后缀。

如果存在p0p1...pk-1pk=pj-kpj-k+1...pj-1pj,那么在包含pj的模式串中有最大长度为k+1的相同前缀后缀。

举个例子,如果给定的模式串为“abab”,那么它的各个子串的前缀后缀的公共元素的最大长度如下表格所示:

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

∙②求next数组

onext数组考虑的是除当前字符外的最长相同前缀后缀,所以通过第①步骤求得各个前缀后缀的公共元素的最大长度后,只要稍作变形即可:

将第①步骤中求得的值整体右移一位,然后初值赋为-1,如下表格所示:

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

∙③根据next数组进行匹配

o匹配失配,j=next[j],模式串向右移动的位数为:

j-next[j]。

换言之,当模式串的后缀pj-kpj-k+1,...,pj-1跟文本串si-ksi-k+1,...,si-1匹配成功,但pj跟si匹配失败时,因为next[j]=k,相当于在不包含pj的模式串中有最大长度为k的相同前缀后缀,即p0p1...pk-1=pj-kpj-k+1...pj-1,故令j=next[j],从而让模式串右移j-next[j]位,使得模式串的前缀p0p1,...,pk-1对应着文本串si-ksi-k+1,...,si-1,而后让pk跟si继续匹配。

如下图所示:

  综上,KMP的next数组相当于告诉我们:

当模式串中的某个字符跟文本串中的某个字符匹配失配时,模式串下一步应该跳到哪个位置。

如模式串中在j处的字符跟文本串在i处的字符匹配失配时,下一步用next[j]处的字符继续跟文本串i处的字符匹配,相当于模式串向右移动j-next[j]位。

  接下来,分别具体解释上述3个步骤。

3.3解释

3.3.1寻找最长前缀后缀

  如果给定的模式串是:

“ABCDABD”,从左至右遍历整个模式串,其各个子串的前缀后缀分别如下表格所示:

  也就是说,原模式串子串对应的各个前缀后缀的公共元素的最大长度表为(下简称《最大长度表》):

3.3.2基于《最大长度表》匹配

   因为模式串中首尾可能会有重复的字符,故可得出下述结论:

失配时,模式串向右移动的位数为:

已匹配字符数-失配字符的上一位字符所对应的最大长度值

  下面,咱们就结合之前的《最大长度表》和上述结论,进行字符串的匹配。

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

     

∙1.因为模式串中的字符A跟文本串中的字符B、B、C、空格一开始就不匹配,所以不必考虑结论,直接将模式串不断的右移一位即可,直到模式串中的字符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减去上一位字符B对应的最大长度2,即向右移动6-2=4位。

      

∙6.经历第5步后,发现匹配成功,过程结束。

      

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

而这个最大长度便正是next数组要表达的含义。

3.3.3根据《最大长度表》求next数组

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

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

∙失配时,模式串向右移动的位数为:

已匹配字符数 -失配字符的上一位字符所对应的最大长度值

  上文利用这个表和结论进行匹配时,我们发现,当匹配到一个字符失配时,其实没必要考虑当前失配的字符,更何况我们每次失配时,都是看的失配字符的上一位字符对应的最大长度值。

如此,便引出了next数组。

  给定字符串“ABCDABD”,可求得它的next数组如下:

  把next数组跟之前求得的最大长度表对比后,不难发现,next数组相当于“最大长度值”整体向右移动一位,然后初始值赋为-1。

意识到了这一点,你会惊呼原来next数组的求解竟然如此简单:

就是找最大对称长度的前缀后缀,然后整体右移一位,初值赋为-1(当然,你也可以直接计算某个字符对应的next值,就是看这个字符之前的字符串中有多大长度的相同前缀后缀)。

  换言之,对于给定的模式串:

ABCDABD,它的最大长度表及next数组分别如下:

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

失配时,模式串向右移动的位数为:

失配字符所在位置 -失配字符对应的next值

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

为什么呢?

因为:

∙根据《最大长度表》,失配时,模式串向右移动的位数=已经匹配的字符数-失配字符的上一位字符的最大长度值

∙而根据《next数组》,失配时,模式串向右移动的位数=失配字符的位置-失配字符对应的next值

o其中,从0开始计数时,失配字符的位置=已经匹配的字符数(失配字符不计数),而失配字符对应的next值= 失配字符的上一位字符的最大长度值,两相比较,结果必然完全一致。

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

3.3.4通过代码递推计算next数组

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

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

∙1.如果对于值k,已有p0p1,...,pk-1=pj-kpj-k+1,...,pj-1,相当于next[j]=k。

o此意味着什么呢?

究其本质,next[j]=k代表p[j]之前的模式串子串中,有长度为k的相同前缀和后缀。

有了这个next数组,在KMP匹配中,当模式串中j处的字符失配时,下一步用next[j]处的字符继续跟文本串匹配,相当于模式串向右移动j-next[j]位。

举个例子,如下图,根据模式串“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个序列字符:

∙若p[k]==p[j],则next[j+1]=next[j]+1=k+1;

∙若p[k]≠p[j],如果此时p[ next[k] ]==p[j],则next[j+1]= next[k] +1,否则继续递归前缀索引k=next[k],而后重复此过程。

 相当于在字符p[j+1]之前不存在长度为k+1的前缀"p0p1,…,pk-1pk"跟后缀“pj-kpj-k+1,…,pj-1pj"相等,那么是否可能存在另一个值t+1

如果存在,那么这个t+1便是next[j+1]的值,此相当于利用已经求得的next数组(next[0,...,k,...,j])进行P串前缀跟P串后缀的匹配。

  一般的文章或教材可能就此一笔带过,但大部分的初学者可能还是不能很好的理解上述求解next数组的原理,故接下来,我再来着重说明下。

  如下图所示,假定给定模式串ABCDABCE,且已知next[j]=k(相当于“p0pk-1”=“pj-kpj-1”=AB,可以看出k为2),现要求next[j+1]等于多少?

因为pk=pj=C,所以next[j+1]=next[j]+1=k+1(可以看出next[j+1]=3)。

代表字符E前的模式串中,有长度k+1的相同前缀后缀。

  但如果pk!

=pj呢?

说明“p0pk-1pk” ≠“pj-kpj-1pj”。

换言之,当pk!

=pj后,字符E前有多大长度的相同前缀后缀呢?

很明显,因为C不同于D,所以ABC跟ABD不相同,即字符E前的模式串没有长度为k+1的相同前缀后缀,也就不能再简单的令:

next[j+1]=next[j]+1。

所以,咱们只能去寻找长度更短一点的相同前缀后缀。

  结合上图来讲,若能在前缀“p0pk-1pk”中不断的递归前缀索引k=next[k],找到一个字符pk’也为D,代表pk’=pj,且满足p0pk'-1pk'=pj-k'pj-1pj,则最大相同的前缀后缀长度为k'+1,从而next[j+1]=k’+1=next[k']+1。

否则前缀中没有D,则代表没有相同的前缀后缀,next[j+1]=0。

   那为何递归前缀索引k=next[k],就能找到长度更小的相同前缀后缀呢?

这又归根到next数组的含义。

为了寻找长度相同的前缀后缀,我们拿前缀p0pk-1pk去跟后缀pj-kpj-1pj匹配,如果pk跟pj失配,下一步就是用p[next[k]]去跟pj继续匹配,如果p[next[k]]跟pj还是不匹配,则下一步用p[next[next[k]]]去跟pj匹配。

相当于在不断的递归k=next[k],直到要么找到长度更小的相同前缀后缀,要么没有长度更小的相同前缀后缀。

  所以,因最终在前缀ABC中没有找到D,故E的next值为0:

模式串的后缀:

ABDE

模式串的前缀:

ABC

前缀右移两位:

   ABC

  读到此,有的读者可能又有疑问了,那能否举一个能在前缀中找到字符D的例子呢?

OK,咱们便来看一个能在前缀中找到字符D的例子,如下图所示:

  给定模式串DABCDABDE,我们很顺利的求得字符D之前的“DABCDAB”的各个子串的最长相同前缀后缀的长度分别为0000123,但当遍历到字符D,要求包括D在内的“DABCDABD”最长相同前缀后缀时,我们发现pj处的字符D跟pk处的字符C不一样,换言之,前缀DABC的最后一个字符C跟后缀DABD的最后一个字符D不相同,所以不存在长度为4的相同前缀后缀。

  怎么办呢?

既然没有长度为4的相同前缀后缀,咱们可以寻找长度短点的相同前缀后缀,最终,因在p0处发现也有个字符D,p0=pj,所以p[j]对应的长度值为1,相当于E对应的next值为1。

  综上,可以通过递推求得next数组,代码如下所示:

1.void GetNext(char* p,int next[])  

2.{  

3.    int pLen = strlen(p);  

4.    next[

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 法律文书 > 调解书

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1