(11)串的模式匹配:朴素的模式匹配算法,KMP算法

子串的定位操作通常称为串的模式匹配,它是各种串处理系统中最重要的操作之一,例如很多软件,若有“编辑”菜单项的话,则其中必有“查找”子菜单项。子串定位算法即称为模式匹配算法。

朴素的模式匹配算法

我上一篇文章写的堆存储的串就是用的朴素的模式匹配算法。这种算法很简单,就是子串和主串进行对应位置的匹配,如果发生失配,那么主串和子串都进行回溯,主串回溯到开始匹配位置数目加一,子串回溯到零。
代码如下:
[cpp]  view plain  copy
  1. void HStringIndex(HString *s,HString *t) {                  
  2.     int i = 0,j = 0;    
  3.     while (i < s->length &&j < t->length) {    
  4.         if (s->str[i] == t->str[j]) {    
  5.             i++;     
  6.             j++;    
  7.         }    
  8.         else {    
  9.             i = i - j + 1;    
  10.             j = 0;    
  11.         }    
  12.      }    
  13.     if (j == t->length) {    
  14.         cout << "子串的位置为:" << i-t->length + 1 << endl;    
  15.     }    
  16.     else {    
  17.         cout << "没有该子串!" << endl;    
  18.     }    
  19. }    
这个看看就能理解了。


KMP算法

这个“看毛片”算法真难弄,我先前看了好几篇博客还是有点懵。有些人写得太复杂了,推理过程一大堆,我顺着过程反而越绕越深,本来看东西就不细致的我更加不想看了。弄了几天,我不敢说全部,大部分是懂了。下面我就以我最简单明了的方法来说说我的理解,如果有错误,还请大家能指出来,共同进步嘛。

KMP算法最难理解的其实是next数组的计算方法,核心也是他,弄懂了next数组,KMP就基本没什么问题了。我看到其它博客都是从介绍KMP的用法开始,再介绍next数组的原理。我也按照这个思路来写吧,你们看到也好 理解。

KMP算法是由K什么,P什么,M什么三个人发明的一种对朴素的模式匹配算法的改进算法,它的特点是主串无需回溯。它只需要模式串按照next数组存储的数字移动,用模式串相应位置的字符和主串进行比较就可以了。举个书上的例子把:
        主串:acabaabaabcacaabc
     模式串:abaabc
  next数组:(这里存储的是模式串失配时,下一匹配字符的位置
                 
上面的next数组我会在下面详细介绍怎么来的,你也可以自己去推。现在只需要知道有这么个东西,怎么在KMP算法里面用它就可以了。下面我会贴出代码,你可以推理过程,代码一起看,这样容易理解。
[cpp]  view plain  copy
  1. void KMP(HString *s,HString *t) {<span>     </span>//s是主串,t是模式串  
  2.     int i=0, j=1;  
  3.     while (i < s->length && j <= t->length) {   //只要i,j小于各自串的长度,说明它们还没有匹配完  
  4.         if (j == 0 || s->str[i] == t->str[j - 1]) {     
  5.             i++; j++;       //如果j==0,说明该位置字符与模式串首字符失配,都自增一。  
  6.         }                       //第二个条件满足,说明对应位置字符相同,继续比较后继字符  
  7.         else j = nextNum[j];            //否则模式串移动相应的个数  
  8.     }  
  9.     if (j>t->length) {              
  10.         cout << "子串的位置为:  " << i - t->length + 1 << endl;  
  11.     }  
  12.     else {  
  13.         cout << "没有该子串!" << endl;  
  14.     }  
  15. }  
这里需要注意,nextNum[]就是next数组,因为命名冲突,所以没有直接起。字符数组是从0开始的,而nextNum[ ]是从1开始的,nextNum[0]并没有使用。




知道怎么用了,我们再讲讲原理。模式串字符的排列我们是已知的,但是如何去利用它呢?在这种具有重复字符的模式串里面,我们总能找到相同的部分。比如上图第三趟匹配到第四趟匹配过程中,他们的相同部分是a b。

有相同部分再进行匹配是不是比失配就回溯节约时间。现在关键来了,我们都是利用模式串预先计算出来的next数组来实现的,那我们怎么来计算next数组里面的值呢?

我先贴出代码:
[cpp]  view plain  copy
  1. void Next(HString *t) {  
  2.     int i = 1, j = 0;  
  3.     nextNum[1] = 0;  
  4.     while (i<t->length) {  
  5.         if (j == 0 || t->str[i - 1] == t->str[j - 1]) {  
  6.             ++i;  
  7.             ++j;  
  8.             nextNum[i] = j;<span>       </span>  
  9.         }  
  10.         else {  
  11.             j = nextNum[j];<span>   </span>//回溯,在新的子串里面查找新的最大相同子串  
  12.         }  
  13.     }  
  14. }  
在这个算法里面所求得的nextNum数组,nextNum[1]=0,nextNum[2]=1,这是默认的。
很显然,当模式串第一个字符都失配,我不能让模式串去移动下一位,那只能主串匹配位置自增一,继续匹配。
当模式串第二个字符失配时,那只能看第一个字符能否匹配,如果可以,继续匹配后继字符;不能,那主串匹配位置自增一。
核心就是从next数组的第三位值开始,值得大小取决于之前的字符从首尾开始组成最大的相同子串的长度,如果找到,那么next值是该长度加1,否则next值是1。


例如 :         模式串                 a   b   a   a   b   c
                 nextNum[]          0   1    1   2   2  3 
1)第二个字符失配也可以这么理解,字符组合:{0 | a}  最大相同子串为空,nextNum[2]=0+1;
2)第三个字符,  字符组合                    {0,a| b,0}.                   最大相同子串为空,nextNum[3]=0+1;
3)第四个字符,字符组合              {0,a,ab | ba,a,0}.              最大相同子串为a,长度为1,nextNum[4]=1+1;
5)第五个字符,字符组合        {0,a,ab,aba | baa,aa,a,0}.       最大相同子串为a,长度为1,nextNum[5]=1+1;
6)第六个字符,字符组合{0,a,ab,aba,abaa|baab,aab,ab,a,0}最大相同子串为ab,长度为2,nextNum[6]=2+1;
花括号前半部分是除尾字符的所有子串字符组合,后半部分是除首字符的所有子串组合。

KMP完整代码:
[cpp]  view plain  copy
  1. #include<iostream>  
  2. using namespace std;  
  3. #define max 100  
  4. int nextNum[max];  
  5. typedef struct {  
  6.     int length;  
  7.     char *str;  
  8. }HString;  
  9. void Next(HString *t) {  
  10.     int i = 1, j = 0;  
  11.     nextNum[1] = 0;  
  12.     while (i<t->length) {  
  13.         if (j == 0 || t->str[i - 1] == t->str[j - 1]) {  
  14.             ++i;  
  15.             ++j;  
  16.             nextNum[i] = j;  
  17.         }  
  18.         else {  
  19.             j = nextNum[j];  
  20.         }  
  21.     }  
  22. }  
  23. void KMP(HString *s,HString *t) {  
  24.     int i=0, j=1;  
  25.     while (i < s->length && j <= t->length) {   //只要i,j小于各自串的长度,说明它们还没有匹配完  
  26.         if (j == 0 || s->str[i] == t->str[j - 1]) {     
  27.             i++; j++;               //如果j==0,说明该位置字符与模式串首字符失配,都自增一。  
  28.         }       //第二个条件满足,说明对应位置字符相同,继续比较后继字符  
  29.         else j = nextNum[j];            //否则模式串移动相应的个数  
  30.     }  
  31.     if (j>t->length) {              
  32.         cout << "子串的位置为:  " << i - t->length + 1 << endl;  
  33.     }  
  34.     else {  
  35.         cout << "没有该子串!" << endl;  
  36.     }  
  37. }  
  38. void main() {  
  39.     HString s1,s2;  
  40.     s1.length = 17;  
  41.     s1.str = (char*)malloc(sizeof(char));  
  42.     s1.str = "acabaabaabcacaabc";  
  43.     s2.length = 6;  
  44.     s2.str = (char*)malloc(sizeof(char));  
  45.     s2.str = "adacba";  
  46.     Next(&s2);  
  47.     KMP(&s1, &s2);  
  48.     for (int i = 1; i < 7; i++) {  
  49.         cout << nextNum[i];  
  50.     }  
  51.     system("pause");  
  52. }  
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值