字符串匹配:KMP、BM

来源:互联网 发布:弓弦逸鹤 知乎 编辑:程序博客网 时间:2024/06/05 00:54

转载自: http://blog.csdn.net/linraise/article/details/9899397


一.暴力匹配法

最原始,最直观的办法,就是蛮力搜索法,思路是这样子的,需要在str1中寻找str2,那么可以先在str1中查找str2[0],如果找到,则比较往后的字符,如果全匹配,则返回一开始的符号,如果不匹配,继续在str1中找str2[0],一直重复以上步骤,直至找到为止.分析这种办法的时间复杂度.在最差的情况下,例如长度为m的字符串0000000000,和长度为n的字符串00001,那么显然时间效率为o(mn).在这里我们可以知道,具体的代码实现应该需要双层嵌套.(这种直觉往往能带来帮助).外层用来扫描str1,内层用来实现str2.代码如下:

int Match1(char* str1,char* str2)  {      int j=0;      for(int i=0;str1[i] != '\0';++i)      {          while( str2[j] != '\0' &&str1[i+j] !='\0' && str1[i+j] == str2[j])              ++j;          if(str2[j]=='\0')              return i;          j=0;      }      return -1;  }

为了和KMP算法对比,写成以下形式

int Match2(char* str1,char* str2)  {      int i=0,j=0;      while(str1[i] !='\0' && str2[j] != '\0')      {          if(str1[i] == str2[j])//匹配          {              ++i;              ++j;          }          else          {              i=i-j+1;//回退str1的指针              j=0;//str2从0开始          }      }      if(str2[j] == '\0')          return i-j;//匹配成功      else           return -1;    }  



二.KMP算法

转载自一篇比较浅显易懂的文章:点击打开链接

字符串匹配是计算机的基本任务之一。

举例来说,有一个字符串"BBC ABCDAB ABCDABCDABDE",我想知道,里面是否包含另一个字符串"ABCDABD"?

许多算法可以完成这个任务,Knuth-Morris-Pratt算法(简称KMP)是最常用的之一。它以三个发明者命名,起头的那个K就是著名科学家Donald Knuth。

这种算法不太容易理解,网上有很多解释,但读起来都很费劲。直到读到Jake Boxer的文章,我才真正理解这种算法。下面,我用自己的语言,试图写一篇比较好懂的KMP算法解释。

1.

首先,字符串"BBC ABCDAB ABCDABCDABDE"的第一个字符与搜索词"ABCDABD"的第一个字符,进行比较。因为B与A不匹配,所以搜索词后移一位。

2.

因为B与A不匹配,搜索词再往后移。

3.

就这样,直到字符串有一个字符,与搜索词的第一个字符相同为止。

4.

接着比较字符串和搜索词的下一个字符,还是相同。

5.

直到字符串有一个字符,与搜索词对应的字符不相同为止。

6.

这时,最自然的反应是,将搜索词整个后移一位,再从头逐个比较。这样做虽然可行,但是效率很差,因为你要把"搜索位置"移到已经比较过的位置,重比一遍。

7.

一个基本事实是,当空格与D不匹配时,你其实知道前面六个字符是"ABCDAB"。KMP算法的想法是,设法利用这个已知信息,不要把"搜索位置"移回已经比较过的位置,继续把它向后移,这样就提高了效率。

8.

怎么做到这一点呢?可以针对搜索词,算出一张《部分匹配表》(Partial Match Table)。这张表是如何产生的,后面再介绍,这里只要会用就可以了。

9.

已知空格与D不匹配时,前面六个字符"ABCDAB"是匹配的。查表可知,最后一个匹配字符B对应的"部分匹配值"为2,因此按照下面的公式算出向后移动的位数:

  移动位数 = 已匹配的字符数 - 对应的部分匹配值

因为 6 - 2 等于4,所以将搜索词向后移动4位。

10.

因为空格与C不匹配,搜索词还要继续往后移。这时,已匹配的字符数为2("AB"),对应的"部分匹配值"为0。所以,移动位数 = 2 - 0,结果为 2,于是将搜索词向后移2位。

11.

因为空格与A不匹配,继续后移一位。

12.

逐位比较,直到发现C与D不匹配。于是,移动位数 = 6 - 2,继续将搜索词向后移动4位。

13.

逐位比较,直到搜索词的最后一位,发现完全匹配,于是搜索完成。如果还要继续搜索(即找出全部匹配),移动位数 = 7 - 0,再将搜索词向后移动7位,这里就不再重复了。

14.

下面介绍《部分匹配表》是如何产生的。

首先,要了解两个概念:"前缀"和"后缀"。 "前缀"指除了最后一个字符以外,一个字符串的全部头部组合;"后缀"指除了第一个字符以外,一个字符串的全部尾部组合。

15.

"部分匹配值"就是"前缀"和"后缀"的最长的共有元素的长度。以"ABCDABD"为例,

  - "A"的前缀和后缀都为空集,共有元素的长度为0;

  - "AB"的前缀为[A],后缀为[B],共有元素的长度为0;

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

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

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

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

  - "ABCDABD"的前缀为[A, AB, ABC, ABCD, ABCDA, ABCDAB],后缀为[BCDABD, CDABD, DABD, ABD, BD, D],共有元素的长度为0。

16.

"部分匹配"的实质是,有时候,字符串头部和尾部会有重复。比如,"ABCDAB"之中有两个"AB",那么它的"部分匹配值"就是2("AB"的长度)。搜索词移动的时候,第一个"AB"向后移动4位(字符串长度-部分匹配值),就可以来到第二个"AB"的位置。

(完)

下面转自:点击打开链接

1、用一个例子来解释,下面是一个子串的next数组的值,可以看到这个子串的对称程度很高,所以next值都比较大。

位置i

 0   

 1   

 2   

 3  

 4  

 5  

 6  

 7  

 8  

  9 

 10 

11 

12 

13  

14 

 15 

前缀next[i]

 0

  0

 0

 0

 1

 2

 3

 1

 2

  3

  4

  5

  6

  7

  4

  0

子串

 a

 g

  c

  t

  a

  g

  c

  a

 g

 c

  t

  a

  g

  c

  t

  g

申明一下:下面说的对称不是中心对称,而是中心字符块对称,比如不是abccba,而是abcabc这种对称。

(1)逐个查找对称串。

这个很简单,我们只要循环遍历这个子串,分别看前1个字符,前2个字符,3个... i个 最后到15个。

第1个a无对称,所以对称程度0

前两个ag无对称,所以也是0

依次类推前面0-4都一样是0

前5个agcta,可以看到这个串有一个a相等,所以对称程度为1前6个agctag,看得到ag和ag对成,对称程度为2

这里要注意了,想是这样想,编程怎么实现呢?

只要按照下面的规则:

a、当前面字符的前一个字符的对称程度为0的时候,只要将当前字符与子串第一个字符进行比较。这个很好理解啊,前面都是0,说明都不对称了,如果多加了一个字符,要对称的话最多是当前的和第一个对称。比如agcta这个里面t的是0,那么后面的a的对称程度只需要看它是不是等于第一个字符a了。

 

b、按照这个推理,我们就可以总结一个规律,不仅前面是0呀,如果前面一个字符的next值是1,那么我们就把当前字符与子串第二个字符进行比较,因为前面的是1,说明前面的字符已经和第一个相等了,如果这个又与第二个相等了,说明对称程度就是2了。有两个字符对称了。比如上面agctag,倒数第二个a的next是1,说明它和第一个a对称了,接着我们就把最后一个g与第二个g比较,又相等,自然对称成都就累加了,就是2了。

 

c、按照上面的推理,如果一直相等,就一直累加,可以一直推啊,推到这里应该一点难度都没有吧,如果你觉得有难度说明我写的太失败了。

当然不可能会那么顺利让我们一直对称下去,如果遇到下一个不相等了,那么说明不能继承前面的对称性了,这种情况只能说明没有那么多对称了,但是不能说明一点对称性都没有,所以遇到这种情况就要重新来考虑,这个也是难点所在。

 

(2)回头来找对称性

这里已经不能继承前面了,但是还是找对称程度,最愚蠢的做法大不了写一个子函数,查找这个字符串的最大对称程度,怎么写方法很多吧,比如查找出所有的当前字符串,然后向前走,看是否一直相等,最后走到子串开头,当然这个是最蠢的,我们一般看到的KMP都是优化过的,因为这个串是有规律的。

在这里依然用上面表中一段来举个例子:   

位置i=0到14如下,我加的括号只是用来说明问题:

(a g c t a g c )( a g c t a g c) t

我们可以看到这段,最后这个t之前的对称程度分别是:1,2,3,4,5,6,7,倒数第二个c往前看有7个字符对称,所以对称为7。但是到最后这个t就没有继承前面的对称程度next值,所以这个t的对称性就要重新来求。

这里首要要申明几个事实

1、t 如果要存在对称性,那么对称程度肯定比前面这个c 的对称程度小,所以要找个更小的对称,这个不用解释了吧,如果大那么t就继承前面的对称性了。

2、要找更小的对称,必然在对称内部还存在子对称,而且这个t必须紧接着在子对称之后。

如下图说明。

 

从上面的理论我们就能得到下面的前缀next数组的求解算法。

void SetPrefix(const char *Pattern, int prefix[])

{

     int len=CharLen(Pattern);//模式字符串长度。

     prefix[0]=0;

     for(int i=1; i<len; i++)

     {

         int k=prefix[i-1];

         //不断递归判断是否存在子对称,k=0说明不再有子对称,Pattern[i] != Pattern[k]说明虽然对称,但是对称后面的值和当前的字符值不相等,所以继续递推

         while( Pattern[i] != Pattern[k]  &&  k!=0 )               

             k=prefix[k-1];     //继续递归

         if( Pattern[i] == Pattern[k])//找到了这个子对称,或者是直接继承了前面的对称性,这两种都在前面的基础上++

              prefix[i]=k+1;

         else

              prefix[i]=0;       //如果遍历了所有子对称都无效,说明这个新字符不具有对称性,清0

     }

}

KMP总结

下面是我根据上面两篇博客和数据结构-严蔚敏一书得出的一些心得体会.

KMP算法的核心是next数组的构造.next数组的构造本质上就是查找首尾对称的最长子串长度.

void getNext()

初始化:next[0]=-1,next[1]=0.

假设next[i] == j,则p[0…j-1]=p[i-j…i-1],.那么求next[i+1]有两种情况:

1)如果p[j]=p[i],则p[0…j]=p[i-j…i]所以next[i+1]=j+1=next[i]+1;

2)如果p[j]!=p[i],(难点)

解释:

如果p[j]!=p[i],则说明i+1号元素的对称子串比i号元素的对称子串要短.

看上面的(2)回头来找对称性 我们可以得到启发:

举例:

(a g c t a g c )( a g c t a g c) t

1.t如果要存在对称性,那么对称程度肯定比前面这个c的对称程度小,所以要找个更小的对称,因为如果大,那么t就继承前面的对称性了。

2.要找更小的对称,必然在对称内部还存在子对称,而且这个t必须紧接着在子对称之后。





上面的图示找最长对称子串的步骤:


14号元素是t, next[14]=7,判断string[14] == string[7] (a != t)不成立,继续递归下降查找.

next[7]=3,判断string[14]==string[3],成立,则找到了最长子串.

由上面的分析,我们可以知道,"在内部对称中寻找更小的内部对称,就是递归下降的思想,因此是可以用递归来求解的"

getNext()函数的理解难点是j=next[j]的递归下降查找,由上图就可以清晰地理解.

特点:KMP的串如何存在对称,则肯定是嵌套对称,即大的对称串中必然存在小的对称串,所以可以从大的对称串递归下降地求小的对称串.

KMP算法完整测试代码

#include<iostream>  using namespace std;  void getNext(const char *p,int *next)  {      int i,j;      next[0]=-1;      i=0;      j=-1;      while(i<(signed)strlen(p)-1)      {          if(j==-1||p[i]==p[j])    //1.j==-1说明不再有子对称,则next[i+1]=0          {                        //2.匹配的情况下,p[j]==p[k],则next[i+1]=j+1;              i++;              j++;              next[i]=j;          }          else            //while(j!=-1 && p[i]!=p[j]),一直递归查找,化整为零              j=next[j];  //一直在对称内部中查找子对称,直至不再有子对称(j==-1),或者p[i]==p[j]时,就更可以更新next[i+1]      }  }    int KMP(const char * s, const char *p)  {      int slen=strlen(s),plen=strlen(p);      int *next = (int*)malloc(sizeof(int)*plen);      getNext(p,next);      int i=0,j=0;      while(i<slen && j<plen)      {          if(j==-1|| s[i]==p[j])///j=-1时,也是进行下一轮匹配          {              i++;              j++;          }          else          {              j = next[j];///i,j不再回溯,j有可能=-1          }      }      free(next);      if(j==plen)  return i-j;      else       return -1;  }  void main()  {      char p[]="bcde";      char s[]="edabdabcdeedbd";      int i=KMP(s,p);      cout<<i<<endl;  }  

实训:http://acm.hdu.edu.cn/showproblem.php?pid=1358

这个题主要考察kmp算法中next数组的应用,大致思路是这样的:求出next数组后(next[0]~next[len]),从i=2(也就是第三个 字符)开始,

令j=i-next[i],j可以整除i,则说明存在,输出i和i/j;为什么要这样做呢,j=i-next[i]就是看i和next[i] 之间有多少个字母,如果i%j==0,

则说明i之前一定有一个周期性的字串,长度为i-j,次数为i/j.例如:aabaabaabaab

#include<stdio.h>  #include<string.h>  char pattern[1000001];  int next[1000001];  int len;    void getNext()  {      int i=0,j=-1;      next[0]=-1;      while( i < len )      {          if( j == -1 || pattern[j] == pattern[i] )          {              ++i,++j;              next[i]=j;          }          else          {              j=next[j];          }      }  }  int main()  {      int t=1,i,j;      while(scanf("%d",&len)&&len)      {          getchar();          scanf("%s",pattern);          getNext();          printf("Test case #%d\n",t++);          for(i=2;i<=len;i++)          {              j=i-next[i];              if(i%j==0)              {                  if(i/j>1) printf("%d %d\n",i,i/j);              }          }          printf("\n");      }      return 0;  }  

实训:HDU1711:http://acm.hdu.edu.cn/showproblem.php?pid=1711
完全的KMP.
[cpp] view plaincopy
  1. #include<iostream>  
  2. using namespace std;  
  3.   
  4. int next[10005],pLen,sLen;  
  5. int str[1000000],pattern[10005];  
  6.   
  7. void getNext()  
  8. {  
  9.     int i=0,j=-1;  
  10.     next[0]=-1;  
  11.     while( i < pLen )  
  12.     {  
  13.         if( j==-1 || pattern[i] == pattern[j])  
  14.         {  
  15.             ++i,++j;  
  16.             next[i]=j;  
  17.         }  
  18.         else  
  19.         {  
  20.             j=next[j];  
  21.         }  
  22.     }  
  23. }  
  24. int KMP()  
  25. {  
  26.     int i=0,j=0;  
  27.     getNext();  
  28.     while( i < sLen && j < pLen )  
  29.     {  
  30.         if( j== -1 || str[i] == pattern[j] )  
  31.         {  
  32.             ++i;  
  33.             ++j;  
  34.         }  
  35.         else  
  36.         {  
  37.             j=next[j];  
  38.         }  
  39.     }  
  40.     if( j == pLen )  
  41.         return i-j+1;  
  42.     else   
  43.         return -1;  
  44. }  
  45. int main()  
  46. {  
  47.     int T,i;  
  48.     cin>>T;  
  49.     while(T--)  
  50.     {  
  51.         cin>>sLen>>pLen;  
  52.         for(i=0;i<sLen;++i)  
  53.             cin>>str[i];  
  54.         for(i=0;i<pLen;++i)  
  55.             cin>>pattern[i];  
  56.         cout<<KMP()<<endl;  
  57.     }  
  58.     return 0;  
  59. }  

BM算法

来源:点击打开链接,个中内容有改动

后缀匹配,是指模式串的比较从右到左,模式串的移动也是从左到右的匹配过程,经典的BM算法其实是对后缀蛮力匹配算法的改进。所以还是先从最简单的后缀蛮力匹配算法开始。下面直接给出伪代码,注意这一行代码:j++;BM算法所做的唯一的事情就是改进了这行代码,即模式串不是每次移动一步,而是根据已经匹配的后缀信息,从而移动更多的距离。

[cpp] view plaincopy
  1. int Match(const char* pDest, int nDLen, const char* pPattern, int nPLen)  
  2. {  
  3.     if (0 == nPLen)//空字符返回-1  
  4.         return -1;  
  5.     int nDstart = nPLen-1;  
  6.     while (nDstart < nDLen)  
  7.     {  
  8.         int suffLen = 0;//统计好后缀的长度  
  9.         while (suffLen < nPLen && pDest[nDstart-suffLen] == pPattern[nPLen-1-suffLen])  
  10.             ++suffLen ;  
  11.         if (suffLen == nPLen)  
  12.         {  
  13.             return nDstart - (nPLen-1);//匹配  
  14.         }  
  15.         ++nDstart;  
  16.     }  
  17.     return -1;  
  18. }  

为了实现更快地移动模式串,BM算法定义了两个规则,好后缀规则和坏字符规则,如下图可以清晰的看出他们的含义。利用好后缀和坏字符可以大大加快模式串的移动距离,不是简单的++j,而是j+=max (shift(好后缀), shift(坏字符))


shift(坏字符)分为两种情况:

先设计一个数组bmBc['e'],表示坏字符‘e’在模式串中最后一次出现的位置距离模式串末尾的最大长度(如果不出现,则PLen)


  • 坏字符没出现在模式串中,这时可以把模式串移动到坏字符的下一个字符,继续比较,如下图:

Case 1:坏字符不出现在模式串中


安全移动距离=shift(坏字符)=BmBc[T[i]]-(m-i-1)

  • 坏字符出现在模式串中,这时可以把模式串第一个出现的坏字符(从右往左数)和母串的坏字符(b)对齐,当然,这样可能造成模式串倒退移动,如下图:

    Case 2:坏字符出现在模式串中


安全移动距离=shift(坏字符)=BmBc[T[i]]-(m-i-1)

数组bmBc的创建有两点技巧:若字符不在模式串中,则=strlen(pattern),若在模式串中,则=strlen(pattern)-i-1

[cpp] view plaincopy
  1. //求坏字符数组,某一坏字符距离末尾的长度,若某一字符出现多次,取最右边的,例ababab,则BmBc[a]=1,BmBc[b]=0  
  2. void preBmBc(const char *pPattern, int nLen, int BmBc[])  
  3. {  
  4.     for (int i = 0; i < 256; ++i)//char可以用1个字节保存(256)  
  5.     {  
  6.         BmBc[i] = nLen;//初始化为nLen,如果坏字符不在模式串中,那么BmBc[i]=nLen,安全移动距离=nPLen-GoodSuffix  
  7.     }  
  8.     for (int i = 0; i < nLen; ++i)  
  9.     {  
  10.         BmBc[pPattern[i]] = nLen-1-i;//如果坏字符出现在模式串中(以最右一个为准),安全移动距离=nLen-1-i-GoodSuffix(有可能为负值,即倒退)  
  11.     }  
  12. }  

模式串中有多个子串与好后缀完全匹配,选择最左边的子串与好后缀对齐。(case1:完全匹配)再来看如何根据好后缀规则移动模式串,shift(好后缀)分为三种情况:

  • 模式串中有子串匹配上好后缀,此时移动模式串,让该子串和好后缀对齐即可,如果超过一个子串匹配上好后缀,则选择最靠左边的子串对齐。

  • 模式串中没有子串与好后缀完全匹配(除了下图蓝色部分),则在模式串的开头寻找能与好后缀匹配的最大前缀。(类似KMP的找最大对称串)(case2:部分匹配)

  • 模式串中没有子串与好后缀完全匹配,并且在模式串中找不到最长前缀,此时,直接移动模式串到好后缀的下一个字符。(case3:完全不匹配)

为了实现好后缀规则,需要定义一个数组suffix[],其中suffix[i] = s 表示以i为边界,与模式串后缀匹配的最大长度,如下图所示,用公式可以描述:满足P[i-s, i] == P[m-s, m]的最大长度s。

构建suffix数组的代码如下:

[cpp] view plaincopy
  1. //寻找好后缀长度  
  2. void Suffix(const char *pPattern, int nLen, int *pSuffix)  
  3. {  
  4.     if (0 == nLen)  
  5.         return;  
  6.     pSuffix[nLen-1] = 0;//最后一个字符必定为0  
  7.     for(int i = nLen-2 ; i>=0 ; --i)  
  8.     {  
  9.         int suffLen=0 ;//累计后缀长度  
  10.         while(i-suffLen >=0 && pPattern[i-suffLen] == pPattern[nLen-1-suffLen])  
  11.             ++suffLen;  
  12.         pSuffix[i]=suffLen;  
  13.     }  
  14. }  

模式串中有子串匹配上好后缀有了suffix数组,就可以定义bmGs[]数组,bmGs[i] 表示遇到好后缀时,模式串应该移动的距离,其中i表示好后缀前面一个字符的位置(也就是坏字符的位置),构建bmGs数组分为三种情况,分别对应上述的移动模式串的三种情况

  • 模式串中没有子串匹配上好后缀,但找到一个最大前缀

  • 模式串中没有子串匹配上好后缀,但找不到一个最大前缀

构建bmGs数组的代码如下:

[cpp] view plaincopy
  1. void preBmGs(const char *pPattern, int nLen, int BmGs[])  
  2. {  
  3.     if (0 == nLen)   
  4.         return ;  
  5.     //不直接求好后缀数组,因为直接求的话时间复杂度是O(n^2)  
  6.     //我们先计算出pSuffix数组  
  7.     int *pSuffix = new int[nLen];  
  8.   
  9.     Suffix(pPattern, nLen, pSuffix);  
  10.       
  11.     //根据suffix确定好后缀的值,首先全部初始化为nLen  
  12.     //第三种情况,BmGs[i]=strlen(pattern)  
  13.     for (int i = 0; i < nLen; ++i)  
  14.     {  
  15.         BmGs[i] = nLen;  
  16.     }  
  17.     //第一种情况:完全匹配L'(i)  
  18.     //pSuffix[i]为以i为末尾的好后缀长度  
  19.     int nMaxPrefix = 0;//累计最大前缀长度  
  20.     for (int i = 0; i < nLen; ++i)  
  21.     {  
  22.         BmGs[nLen-1-pSuffix[i]] = nLen-1-i;  
  23.         if (pSuffix[i] == i+1)//说明模式串中[0..i]是前缀  
  24.         {  
  25.             nMaxPrefix = i+1;  
  26.         }  
  27.     }  
  28.    
  29.     //第二种情况(最长前缀):部分匹配l'(i) 前缀和后缀的匹配  
  30.     if (nMaxPrefix > 0)  
  31.     {  
  32.         for (int i = nMaxPrefix; i < nLen-1-nMaxPrefix; ++i)  
  33.         {  
  34.             if (BmGs[i] == nLen)//填满中间空白空格的值,因为安全移动距离需要缩小  
  35.             {  
  36.                 BmGs[i] = nLen-nMaxPrefix;//记录的是到末尾的距离  
  37.             }  
  38.         }  
  39.     }  
  40.     delete []pSuffix;  
  41.      
  42. }  

现在BM算法就可以轻易从蛮力法中改进出来:

[cpp] view plaincopy
  1. int BM(const char* pDest, int nDLen, const char* pPattern, int nPLen, int *BmGs, int *BmBc)  
  2. {  
  3.     if (0 == nPLen)//空字符返回-1  
  4.         return -1;  
  5.     int nDstart = nPLen-1;  
  6.     while (nDstart < nDLen)  
  7.     {  
  8.         int suffLen = 0;//统计好后缀的长度  
  9.         while (suffLen < nPLen && pDest[nDstart-suffLen] == pPattern[nPLen-1-suffLen])  
  10.             ++suffLen ;  
  11.         if (suffLen == nPLen)  
  12.         {  
  13.             return nDstart - (nPLen-1);//匹配  
  14.         }  
  15.         nDstart += max(BmGs[nPLen-1-suffLen] , BmBc[pDest[nDstart-suffLen]]-suffLen);//安全移动,BmBc可能倒退(负值)  
  16.     }  
  17.     return -1;  
  18. }  

BM算法完整测试代码

[cpp] view plaincopy
  1. #include <string.h>  
  2. #include<iostream>  
  3. using namespace std;  
  4. //求坏字符数组,某一坏字符距离末尾的长度,若某一字符出现多次,取最右边的,例ababab,则BmBc[a]=1,BmBc[b]=0  
  5. void preBmBc(const char *pPattern, int nLen, int BmBc[])  
  6. {  
  7.     for (int i = 0; i < 256; ++i)//char可以用1个字节保存(256)  
  8.     {  
  9.         BmBc[i] = nLen;//初始化为nLen,如果坏字符不在模式串中,那么BmBc[i]=nLen,安全移动距离=nPLen-GoodSuffix  
  10.     }  
  11.     for (int i = 0; i < nLen; ++i)  
  12.     {  
  13.         BmBc[pPattern[i]] = nLen-1-i;//如果坏字符出现在模式串中(以最右一个为准),安全移动距离=nLen-1-i-GoodSuffix(有可能为负值,即倒退)  
  14.     }  
  15. }  
  16. //寻找好后缀长度  
  17. void Suffix(const char *pPattern, int nLen, int *pSuffix)  
  18. {  
  19.     if (0 == nLen)  
  20.         return;  
  21.     pSuffix[nLen-1] = 0;//最后一个字符必定为0  
  22.     for(int i = nLen-2 ; i>=0 ; --i)  
  23.     {  
  24.         int suffLen=0 ;//累计后缀长度  
  25.         while(i-suffLen >=0 && pPattern[i-suffLen] == pPattern[nLen-1-suffLen])  
  26.             ++suffLen;  
  27.         pSuffix[i]=suffLen;  
  28.     }  
  29. }  
  30.   
  31. void preBmGs(const char *pPattern, int nLen, int BmGs[])  
  32. {  
  33.     if (0 == nLen)   
  34.         return ;  
  35.     //不直接求好后缀数组,因为直接求的话时间复杂度是O(n^2)  
  36.     //我们先计算出pSuffix数组  
  37.     int *pSuffix = new int[nLen];  
  38.   
  39.     Suffix(pPattern, nLen, pSuffix);  
  40.       
  41.     //根据suffix确定好后缀的值,首先全部初始化为nLen  
  42.     //第三种情况,BmGs[i]=strlen(pattern)  
  43.     for (int i = 0; i < nLen; ++i)  
  44.     {  
  45.         BmGs[i] = nLen;  
  46.     }  
  47.     //第一种情况:完全匹配L'(i)  
  48.     //pSuffix[i]为以i为末尾的好后缀长度  
  49.     int nMaxPrefix = 0;//累计最大前缀长度  
  50.     for (int i = 0; i < nLen; ++i)  
  51.     {  
  52.         BmGs[nLen-1-pSuffix[i]] = nLen-1-i;  
  53.         if (pSuffix[i] == i+1)//说明模式串中[0..i]是前缀  
  54.         {  
  55.             nMaxPrefix = i+1;  
  56.         }  
  57.     }  
  58.    
  59.     //第二种情况(最长前缀):部分匹配l'(i) 前缀和后缀的匹配  
  60.     if (nMaxPrefix > 0)  
  61.     {  
  62.         for (int i = nMaxPrefix; i < nLen-1-nMaxPrefix; ++i)  
  63.         {  
  64.             if (BmGs[i] == nLen)//填满中间空白空格的值,因为安全移动距离需要缩小  
  65.             {  
  66.                 BmGs[i] = nLen-nMaxPrefix;//记录的是到末尾的距离  
  67.             }  
  68.         }  
  69.     }  
  70.     delete []pSuffix;  
  71.      
  72. }  
  73.   
  74. int BM(const char* pDest, int nDLen, const char* pPattern, int nPLen, int *BmGs, int *BmBc)  
  75. {  
  76.     if (0 == nPLen)//空字符返回-1  
  77.         return -1;  
  78.     int nDstart = nPLen-1;  
  79.     while (nDstart < nDLen)  
  80.     {  
  81.         int suffLen = 0;//统计好后缀的长度  
  82.         while (suffLen < nPLen && pDest[nDstart-suffLen] == pPattern[nPLen-1-suffLen])  
  83.             ++suffLen ;  
  84.         if (suffLen == nPLen)  
  85.         {  
  86.             return nDstart - (nPLen-1);//匹配  
  87.         }  
  88.         nDstart += max(BmGs[nPLen-1-suffLen] , BmBc[pDest[nDstart-suffLen]]-suffLen);//安全移动,BmBc可能倒退(负值)  
  89.     }  
  90.     return -1;  
  91. }  
  92.   
  93. void TestBM()  
  94. {  
  95.     int         nFind;  
  96.     int         BmGs[100] = {0};  
  97.     int         BmBc[256]  = {0};  
  98.                                //        1         2         3         4  
  99.                                //0123456789012345678901234567890123456789012345678901234  
  100.     const char  dest[]      =   "Hello , My name is LinRaise,welcome to my blog";  
  101.     const char  pattern[][20] = {  
  102.         "H",  
  103.         "He",  
  104.         "Hel",  
  105.         "My",  
  106.         "name",  
  107.         "wel",  
  108.         "blog",  
  109.         "Lin",  
  110.         "Raise",  
  111.         "to",  
  112.         "x",  
  113.         "y",  
  114.         "My name",  
  115.         "to my",  
  116.     };  
  117.     int seco=0;  
  118.     for(int i=0;i<strlen(dest);++i)// destination  
  119.     {  
  120.         cout<<'-'<<dest[i]<<i<<'\t';  
  121.         if(++seco % 10 ==0)  
  122.             cout<<endl;  
  123.     }  
  124.     cout<<endl;  
  125.     for (int i = 0; i < sizeof(pattern)/sizeof(pattern[0]); ++i)  
  126.     {  
  127.         preBmBc(pattern[i], strlen(pattern[i]), BmBc);  
  128.         preBmGs(pattern[i], strlen(pattern[i]), BmGs);  
  129.   
  130.         nFind = BM(dest, strlen(dest), pattern[i], strlen(pattern[i]), BmGs, BmBc);  
  131.         if (-1 != nFind)  
  132.         {  
  133.             printf("Found    \"%s\" at %d \t%s\r\n", pattern[i], nFind, dest+nFind);  
  134.         }  
  135.         else  
  136.         {  
  137.             printf("Found    \"%s\" no result.\r\n", pattern[i]);  
  138.         }  
  139.   
  140.     }  
  141. }  
  142.   
  143. int main(int argc, char* argv[])  
  144. {  
  145.     TestBM();  
  146.       
  147.     return 0;  
  148. }  

再引用另一篇博文:打破思维之BM算法以供参考.考虑模式串匹配不上母串的最坏情况,后缀蛮力匹配算法的时间复杂度最差是O(n×m),最好是O(n),其中n为母串的长度,m为模式串的长度。BM算法时间复杂度最好是O(n/(m+1)),最差是多少?留给读者思考。

参考资料:

上面的做画图示:图示

柔性字符匹配:点击打开链接


原创粉丝点击