KMP模式匹配算法(1)

在写程序时,我们常常会用到从一个字符串找出子串的位置。


比如,在某个句子快速找到某个你感兴趣的词:“csscsdn”中找到csdn


以下是查找算法,注意:下标为1代表第一个位置,以此类推,跟数组不同


一、朴素模式匹配

朴素模式匹配是从第一个字符开始,依次向后匹配,这里我们假设游标起始点为1

c s s c s d n

|| || ?

c s d n

当i(主串的游标) = 3,j(子串的游标) = 3时,字符出现了不等,于是i回到2,j回到1

c s s c s d n

   ?

   c s d n

直到

c s s c s d n

        || || || ||

        c s d n

朴素模式匹配最大的问题就是i值回溯,如果碰到当子串的前面都与主串匹配,只有最后一个字符不同,而这个子串又在主串的末尾时,j值会每次遍历整个字串,i值会每次回溯

000...共n个0...0001

0...共m个0...01

当出现上面的情况时,会发生最坏的情况,此时时间复杂度为O((n-m+1)*m)


二、KMP算法

后来有三位前辈(Knuth、Pratt和Morris)发布了一个大大避免重复遍历的算法,简称KMP算法

1、

c s d c s d n

        ?

c s d n

2、

c s d c s d n

  ?

  c s d n

3、

c s d c s d n

     ?

     c s d n

可以看到,其实2和3步骤完全是没有必要的,因为在第一次比较时csd都是一样的,所以主串中的第一个s必然不等于字串的c,主串中的第一个d也必然不等于子串中的c


再看另一种情况

1、

c s c s c s b n

           ?

c s c s d n

2、

c s c s c s b n

   ?

   c s c s d n

3、

c s c s c s b n

     ||

     c s c s d n


4、

c s c s c s b n

     || || 

     c s c s d n


5、

c s c s c s b n

     || || || 

     c s c s d n

6、

c s c s c s b n

     || || || ||

     c s c s d n


7、

c s c s c s b n

     || || || || ?

     c s c s d n


可以看出,2、3、4步是没有必要的,2步没必要跟上面的情况一致,而3、4没必要,是因为子串在1中已经知道前4个字符都与主串是一样的了,既然即主串中第三和第四个位置上的字符与子串 第三和第四个位置上的字符是一样的,而子串第三和第四个位置上的字符又与子串第一和第二个位置上的字符一样,那子串中第一和第二个位置上的字符必然与主串中第三和第四个位置上的字符一样,所以是不需要3、4步骤的


综上,我们可以总结出两点:1、主串中的i是不需要回溯的,子串中的j需要回溯;2、子串中的j回溯的位置,跟子串字符的重复有关系


接下来,我们需要找出子串字符的重复关系,通过上面c s c s c s b n与 c s c s d n的查找,我们可以看到,如果省略步骤3和4,当i = 5时,是与j = 3的字符来比较的,


我们把子串j值的变化用一个next数组来表示:

j:   1 2 3 4 5 6

T:   c s c s d n

next:  0 1 1 2 3 1

下面来说说为什么这么表示,首先,next[1] = 0,是为了简化判断,这个我们稍后再说;然后看next[5] = 3,之所以等于3,是因为在j = 5不匹配时,我们无需让j回溯到1,只需要回溯到3即可,因为T[1] = T[3],T[2] = T[4],可以推出next[j] = max{k|1<k<j, 且'p->1...p->k-1 = 'p->j-k+1...p->j-1'}(当此集合不为空的时候,这里->代表下标),实际上,k就是子串开头字符重复的个数加1


next数组的生成代码,注意:T[0]表示子串的长度:

[cpp]  view plain copy
  1. void get_next(char *T, int *next) {  
  2.     int i,j;  
  3.     i = 1;  
  4.     j = 0;  
  5.     next[i] = 0;  
  6.       
  7.     while (i < T[0]) {  
  8.         if (j == 0 || T[i] == T[j]) {  
  9.             ++i;  
  10.             ++j;  
  11.             next[i] = j;  
  12.         } else {  
  13.             j = next[j];  
  14.         }  
  15.     }  
  16. }  

有了next数组,KMP实现代码就出来了,注意: T[0]表示子串的长度,S[0]表示主串的长度

[cpp]  view plain copy
  1. int index_KMP(char *S, char *T, int pos) {  
  2.     int i = pos;  
  3.     int j = 1;  
  4.     int next[255];  
  5.       
  6.     get_next(T, next);  
  7.       
  8.     while (i <= S[0] && j <= T[0]) {  
  9.         if (j == 0 || S[i] == T[j]) {  
  10.             ++i;  
  11.             ++j;  
  12.         } else {  
  13.             j = next[j];  
  14.         }  
  15.     }  
  16.       
  17.     if (j > T[0])  
  18.         return i - T[0];  
  19.     else  
  20.         return 0;  
  21. }  

现在我们看到next[1]为0可以简化判断j是否回溯到开头的代码,并且方便的实现从j=1开始向后匹配


这个算法还是有点问题,比如当子串为aaaab情况时,会有重复向前匹配的情况,打算在下一篇详述


本文参考《大话数据结构》,转载请注明出处,多谢!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
KMP算法(Knuth-Morris-Pratt算法)是一种用于解决字符串匹配问题的高效算法。它的主要思想是利用匹配失败时的信息,尽量减少比较次数,提高匹配效率。 KMP算法的核心是构建一个部分匹配表(Partial Match Table),也称为Next数组。这个表记录了在匹配失败时应该将模式串向右移动的位置。 构建部分匹配表的过程如下: 1. 首先,将模式串中的第一个字符的Next值设为0,表示当匹配失败时,模式串不需要移动; 2. 然后,从模式串的第二个字符开始,依次计算Next值; 3. 当第i个字符与前面某个字符相同的时候,Next[i]的值为该字符之前(不包括该字符)的相同前缀和后缀的最大长度; 4. 如果不存在相同的前缀和后缀,则Next[i]的值为0。 有了部分匹配表之后,KMP算法的匹配过程如下: 1. 用i和j来分别表示模式串和主串的当前位置; 2. 如果模式串中的字符和主串中的字符相同,那么i和j都向右移动一位; 3. 如果模式串中的字符和主串中的字符不同,那么根据部分匹配表来确定模式串的下一个位置; 4. 假设当前模式串的位置为i,根据部分匹配表中的值Next[i],将模式串向右移动Next[i]个位置; 5. 重复上述步骤,直到找到匹配或者主串遍历完毕。 KMP算法的时间复杂度为O(m + n),其中m和n分别是模式串和主串的长度。相比于暴力匹配算法的时间复杂度为O(m * n),KMP算法能够大幅减少比较次数,提高匹配效率。 综上所述,KMP模式匹配算法通过构建部分匹配表并利用匹配失败时的信息,实现了高效的字符串匹配。在实际应用中,KMP算法被广泛地应用于文本编辑、数据搜索和字符串处理等领域。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值