在字符串模式匹配的学习中,对于没有学过的数据结构与算法的来讲,可能首先就会想起将模式字符串和目标字符串逐个去比较,直到匹配为止,这就学术上说的“朴素”算法,这算法的确可行,但是不高效,从而有了KMP的算法的出现,简单来讲KMP算法就是利用模式字符和匹配过程的已知条件得出一个值,去跳过在朴素算法逐个匹配过程中无必要的匹配,从而达到高效的算法。虽然这是简单的思路,但是KMP算法理解起来真的比较费劲,下面,我自己结合课件和网上各位大神的解释,总结写一下比较好懂的KMP算法解释。

字符串模式匹配指的是,找出特定的模式串在一个较长的字符串中出现的位置。

  • 朴素的模式匹配算法(BF(Brute Force)算法)

朴素模式匹配算法的基本思想是穷举法,即就是将目标串S的第一个字符与模式串P的第一个字符进行匹配,若相等,则继续比较S的第二个字符和P的第二个字符;若不相等,则比较S的第二个字符和P的第一个字符,依次比较下去,直到得出最后的匹配结果(如图1所示)。

QQ20131110154607_thumb12

图1:朴素的匹配方法示意

朴素的字符串模式匹配算法:

  1. /*  
  2. 函数 int NaiveStringMatching(String T,String P)从目标T的首位位置开始模式匹配,  
  3. 用模式P匹配T,寻找首个P子串并返回其下标位置。若整个匹配过程失败 (模式P移动到目标T  
  4. 尾部位置),则返回负值。  
  5. */ 
  6.  
  7. int NaiveStringMatching(const String &T,const String &P){  
  8.     int i=0;                           //模式的下标变量   
  9.     int j=0;                           //目标的下标变量   
  10.     int pLen=P.length();               //模式的长度   
  11.     int tLen=T.length();               //目标的长度   
  12.     if (tLen<pLen)                     //如果目标比模式短,匹配无法成功   
  13.         return(-1);  
  14.     while (i<pLen&&j<tLen){            //反复比较对应字符来开始匹配   
  15.         if (T[j]==P[i])  
  16.             i++,j++;                   //继续比较后续字符    
  17.         else{                          //指针回溯到 下一首位,重新开始  
  18.             j=j-i+1;  
  19.             i=0;  
  20.         }  
  21.     }  
  22.     if (i>=pLen)  
  23.         return (j-pLen+1);  
  24.     else 
  25.         return (-1);  

 

上面的代码,T就是目标串,p是模式串,其实现思想也很简单:

当T[j] == p[i]时,目标串和模式串的指针都向后移动一位,进行匹配。而当T[j] != p[i]时,即匹配不成功时,将目标串和模式串的指针同时回溯,i = 0 而目标串的指针j则回溯到这轮开始的下一个位置。

朴素的模式匹配的算法复杂度是O( (n-m+1) * m)  n为目标串的长度,m为模式串长度。

从其实现思想上可以很容易的看出,造成该算法低效的地方是在匹配不成功时主串和模式串的指针回溯上。

  • KMP模式匹配算法

 

为了避免指针的回溯,Knuth(D.E.Knuth)、Morris(J.H.Morris)和Pratt(V.R.Pratt)等人,发现其实每次右移的位数存在且与目标串无关,仅仅依赖模式本身,从而进行改进算法。

改进后的算法(简称为:KMP算法)的基本思想为:预先处理模式本身,分析其字符分布状况,并为模式中的每一个字符计算失配时应该右移的位数。这就是所谓的字符串的特征向量。

字符串的特征向量是KMP算法的关键,而这个字符串的特征向量也称为Next数组,所以如果我们可以得出这个Next数组就可以知道每一个字符失配时应该右移的位数。

而问题来了,这个所谓的Next数组(字符串的特征向量)怎么样可以求出?在解决这个问题之前,我们先来理解一下字符串的“前缀子串”和“后缀子串”两个概念。

  • “前缀子串”指除了最后一个字符以外,一个字符串的全部头部组合
  • “后缀子串”指除了第一个字符以外,一个字符串的全部尾部组合。

同时我们还来定义”前缀子串”和”后缀子串”的最长的共有元素的长度为K值,称为特征数,以”ABCDABD”为例:

       1、 ”A”的前缀和后缀都为空集,共有元素的长度为0;

1_thumb9

  2、 ”AB”的前缀为[A],后缀为[B],共有元素的长度为0; 2_thumb11

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

3_thumb1

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

4_thumb1

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

5_thumb1

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

6_thumb1

  到目前为止,应该可以理解特征数K值的求法,而所有的特征数组成就成为我们所求的Next数组(字符串的特征向量)。细心的可以发现模式串P “ABCDABD”中第一“A”下面没有对应的特征数K值。其实此可以填上-1作为特征数,进而可以组成特征向量Next数组={-1,0,0,0,0,1,2},至于为什么是”-1“,而不是其他数呢?下面会有说明。

在说明为什么是”-1“之前,先验证之前说的Next数组就可以知道每一个字符失配时应该右移的位数,下面举一例子以说明:

7_thumb6 如果按照之前所说得朴素模式匹配算法的话,最自然的反应是,将搜索词整个后移一位,再从头逐个比较。9_thumb1

但是,我们已经在上面已经对模式串P进行分析了,还得出了Next数组(字符串的特征向量),不必再去逐个比较,

7_thumb7

 

10_thumb4

 

已知空格与D不匹配时,前面六个字符”ABCDAB”是匹配的。由失配的”D”对应的特征数K值为2,对于的模式串位置为6,即可按照以下公式可算出向后移动的位数:

移动位数 = 失配字符所在的位置i – 对应特征数K

即可移动位数=6-2=4,所以应该向后移动 4 位。同理,当失配时,即可计算出移动位数,直到匹配为止。

11_thumb1

此刻根据公式,即可反向推出为什么首字符对应为“-1”,可以假设当模式串P与目标串一开始就不匹配,易知需要移动一位,

即1(移动位数)=0(失配字符所在的位置i)-对应特征数K

对应特征数K=-1;可见为什么是“-1”即可得出。

由此可以得出模式P的特征向量Next的计算公式:

2_thumb3

下面还是以”ABCDABD”为例解释一下计算公式的使用:1_thumb2

给定的字符串叫做模式串P。i表示next函数的参数,其值是从1到n。而k则表示一种情况下的next函数值。P表示其中的某个字符,下标从1开始。看等式左右对应的字符是否相等。

i=0时,next[0]=-1

i=1时,k的取值为(0,1)的开区间,所以整数k是不存在的,那就是第三种情况,next[1]=0

i=2时,k的取值为(0,2)的开区间,k从最大的开始取值,然后带入含p的式子中验证等式是否成立,不成立k取第二大的值。现在是k=1,将k导入p的式中得,P0=P1,即“A”=“B”,显然不成立,舍去。k再取值就超出范围了,所以next[2]不属于第二种情况,那就是第三种了,即next[2]=0

i=3时,k的取值为(0,3)的开区间,先取k=2,将k导入p的式子中得,P0P1=P1P2,不成立。 再取k=1,得P0=P2,不成立。那就是第三种了,即next[3]=0

i=4时,k的取值为(0,4)的开区间,先取k=3,将k导入p的式子中得,P0P1P2=P1P2P3,不成立。 再取k=2,得P0P1=P2P3,不成立。 再取k=1,得P0=P3,不成立。所以next[4]=0

i=5时,k的取值为(1,5)的开区间,先取k=4,将k导入p的式子中得,P0P1P2P3=P1P2P3P4,不成立。 取k=3,得P0P1P2=P2P3P4,不成立。再取k=2,将k导入p的式子中得,P0P1=P3P4,不成立。再取k=1,将k导入p的式子中得,P0=P4,成立。那就是第二种了 ,所以next[5]=1

i=6时,k的取值为(1,6)的开区间,先取k=5,将k导入p的式子中得,P0P1P2P3P4=P1P2P3P4P5,不成立。 取k=4,得P0P1P2P3=P2P3P4P5,不成立。再取k=3,将k导入p的式子中得,P0P1P2=P3P4P5,不成立。再取k=2,将k导入p的式子中得,P0P1=P4P5,成立。那就是第二种了 ,所以next[6]=2

即可得下表

2_thumb1

其实,在计算的时候就会发现,这也是和之前说得计算特征数K值是一样的思路,进而也得出Next[i]数组={-1,0,0,0,0,1,2},所以当熟悉的时候就可以很快求出模式字符串的Next数组(字符串的特征向量)。

下面再以另外一例子来该next数组还可进一步优化。

目标字符串T为:abacaabaccabacabaa

模式字符串P为:abacab

next向量根据上面的方法可以求出如下表:

1_thumb5

  在上图可以知道,当进行第6次比较的时候,发现此刻是失配的,按照之前的“移动位数 = 失配字符所在的位置i – 对应特征数K”可以得出5-1=4,即右移4位,再次进行第7次比较,其他如此类推进行比较。

这里的确是KMP算法的思想,已经利用模式字符串的特征来跳过不必要的比较,但是细心的可以发现,在第6次比较的时候,目标字符串T中的a的确不等于模式字符串P的j=5位置的b,同时模式字符串P的j=5位置的b,我们可以根据已知的模式字符串P特征,得出j=1位置的b等于j=5位置的b,即可知道j=1位置的b不会等于目标字符串T中的a,当右移4位再次比较,已经没有必要,此刻应该再右移进行比较,如下图。

2_thumb6

当 i = 0时,令next[i] = -1
其实,若pk = pi ,则有pk 不等于Tj;此时应再右移,使pnext[k]与 Tj 比较,故
第2步可进一步优化为:
    

  1. if  (pk==pi)    
  2. next[i] = next[k];  
  3.  else   next[i] = k; 
 

可以得

3_thumb5

简单说明优化Next求法:

i = 0时,依然是-1,而i=1时候,k为0,代入PK得PK=a,而Pi=b,显然是不等

同理即求出其他关系,在这里说明一下,当PK vs Pi 的关系不等的时候,优化后的K值还是优化前的K值,而PK vs Pi 的关系相等,即优化后的K值为PK对应的k值。

计算字符串特征向量(优化)代码:

  1. /*  
  2. 计算字符串特征向量(优化版)   
  3. */ 
  4. int *findNext(String P){  
  5.     int i = 0;  
  6.     int k = -1;  
  7.     int m = P.length();                   //m为字符串P的长度   
  8.     assert(m>0);                          // 若m=0,退出   
  9.     int * next = new int [m];            // 动态存储区开辟整数数组   
  10.     assert (next != 0);                 //若开辟存储区域失败,退出   
  11.     next[0]=-1;  
  12.     while(i<m){                          //计算i=1…m-1的next值   
  13.         while (k>=0&&P[i]!=P[k])  
  14.             k-next[k];  
  15.         i++;  
  16.         k++;  
  17.         if(i==m) break;  
  18.         if(P[i]==P[k])  
  19.             next[i]==next[k];         //P[i]和P[k]相等,优化     
  20.         else 
  21.             next[i]=k;               //不需要优化,就是位置i的首尾子串的长度   
  22.     }   
  23.     return next;  
 

到目前为止,整个KMP算法的思路已经很清楚。

KMP算法代码:

  1. /*  
  2. KMP模式匹配算法的实现   
  3. */ 
  4. int KMPStrMatching(const String &T,const String &P,int *N){    
  5.     int i=0;                           //模式的下标变量   
  6.     int j=0;                           //目标的下标变量   
  7.     int pLen=P.length();              //模式的长度   
  8.     int tLen=T.length();              //目标的长度   
  9.     if (tLen<pLen)                    //如果目标比模式短,匹配无法成功   
  10.         return(-1);  
  11.     while (i<pLen&&j<tLen){           //反复比较对应字符来开始匹配   
  12.         if(i==-1||T[j]==P[i])  
  13.             i++,j++;  
  14.         else 
  15.             i=N[i];  
  16.     }  
  17.     if (i>plen)  
  18.         return (j-plen+1);  
  19.     else 
  20.         return (-1);  

 

kmp的应用优势:

①高效,O(m+n)的线性最坏时间复杂度;

②无需回朔访问待匹配字符串S.

 

总结:

小弟刚刚开始学数据结构与算法,可能文章某些地方有所欠缺与不足,请忘原谅并多加指点,同时本文参考了阮一峰,崔成龙和waytofall等的博文,同时在此基础上有所修改,总体思路还是一样,为了表示尊重,已经在最后列出原博文地址。最后感悟的是,站在大神的肩膀上看得更远。

 

 

参考资料:

[1]张铭 王腾蛟 赵海燕 数据结构与算法 高等教育出版社

[2] 阮一峰 http://www.ruanyifeng.com/blog/2013/05/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm.html

[3] 崔成龙 http://blog.csdn.net/xiaoxian8023/article/details/8134292

[4] waytofall http://www.cnblogs.com/waytofall/archive/2012/10/27/2742163.html

版权声明:本文为yahong原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/yahong/p/3420565.html