字符串匹配的KMP算法和Boyer-Moore算法

字符串匹配的KMP算法和Boyer-Moore算法

转自  阮一峰


字符串匹配的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"的位置。

附上 KMP算法模板:(链接:ACM!荣耀之路!

<span style="font-size:18px;">    int next[N];  
    char str1[M],str2[N];  
    //str1 长,str2 短  
    //len1,len2,对应str1,str2的长  
      
    void get_next(int len2)  
    {  
        int i = 0,j = -1;  
        next[0] = -1;  
        while(i<len2)  
        {  
            if(j == -1 || str2[i] == str2[j])  
            {  
                i++;  
                j++;  
                if(str2[i] != str2[j])  
                    next[i] = j;  
                else  
                    next[i] = next[j];  
            }  
            else  
                j = next[j];  
        }  
        //计算某字符串的周期,如aaaa是4,abcd是1  
        /* 
        int i = 0;j = -1; 
        next[0] = -1; 
        while(str2[i]) 
        { 
            if(j == -1 || str2[i] == str2[j]) 
            { 
                i++;j++; 
                next[i] = j; 
            } 
            else 
            j = next[j]; 
        } 
        len = strlen(str); 
        i = len-j; 
        if(len%i==0) 
        return len/i; 
        else 
        return 1; 
        */  
    }  
      
    int kmp(int len1,int len2)  
    {  
        int i = 0,j = 0;  
        get_next(len2);  
        while(i<len1)  
        {  
            if(j == -1 || str1[i] == str2[j])  
            {  
                i++;  
                j++  
            }  
            else  
                j = next[j];  
            /* 
                    if(j == len2)//计算str2在str1中出现多少次 
                    { 
            cnt++; 
            j= next[j]; 
                    } 
                    */  
        }  
        //return j; //j为匹配的长度  
        if(j>len2)  
            return 1;//这里也可以返回i-len2来获得匹配在主串中开始的位置  
        else  
            return 0;  
    }  
      
    //数字KMP  
    int a[1000005],b[10005];  
    int next[10005],n,m;  
      
    void getnext()  
    {  
        int i = 0,j = -1;  
        next[0] = -1;  
        while(i<m)  
        {  
            if(j == -1 || b[i] == b[j])  
            {  
                i++;  
                j++;  
                if(b[i] == b[j])  
                next[i] = next[j];  
                else  
                next[i] = j;  
            }  
            else  
            j = next[j];  
        }  
    }  
      
    int kmp()//返回匹配位置  
    {  
        int i = 0,j = 0;  
        while(i<n)  
        {  
            if(a[i] == b[j])  
            {  
                if(j == m-1)  
                return i-j+1;  
                i++;  
                j++;  
            }  
            else  
            {  
                j = next[j];  
                if(j == -1)  
                {  
                    i++;  
                    j = 0;  
                }  
            }  
        }  
        return -1;  
    }  </span>


字符串匹配的Boyer-Moore算法


上面介绍了KMP算法

但是,它并不是效率最高的算法,实际采用并不多。各种文本编辑器的"查找"功能(Ctrl+F),大多采用Boyer-Moore算法

Boyer-Moore算法不仅效率高,而且构思巧妙,容易理解。1977年,德克萨斯大学的Robert S. Boyer教授和J Strother Moore教授发明了这种算法。

下面,我根据Moore教授自己的例子来解释这种算法。

1.

假定字符串为"HERE IS A SIMPLE EXAMPLE",搜索词为"EXAMPLE"。

2.

首先,"字符串"与"搜索词"头部对齐,从尾部开始比较。

这是一个很聪明的想法,因为如果尾部字符不匹配,那么只要一次比较,就可以知道前7个字符(整体上)肯定不是要找的结果。

我们看到,"S"与"E"不匹配。这时,"S"就被称为"坏字符"(bad character),即不匹配的字符。我们还发现,"S"不包含在搜索词"EXAMPLE"之中,这意味着可以把搜索词直接移到"S"的后一位。

3.

依然从尾部开始比较,发现"P"与"E"不匹配,所以"P"是"坏字符"。但是,"P"包含在搜索词"EXAMPLE"之中。所以,将搜索词后移两位,两个"P"对齐。

4.

我们由此总结出"坏字符规则"

  后移位数 = 坏字符的位置 - 搜索词中的上一次出现位置

如果"坏字符"不包含在搜索词之中,则上一次出现位置为 -1。

以"P"为例,它作为"坏字符",出现在搜索词的第6位(从0开始编号),在搜索词中的上一次出现位置为4,所以后移 6 - 4 = 2位。再以前面第二步的"S"为例,它出现在第6位,上一次出现位置是 -1(即未出现),则整个搜索词后移

6 - (-1) = 7位。

5.

依然从尾部开始比较,"E"与"E"匹配。

6.

比较前面一位,"LE"与"LE"匹配。

7.

比较前面一位,"PLE"与"PLE"匹配。

8.

比较前面一位,"MPLE"与"MPLE"匹配。我们把这种情况称为"好后缀"(good suffix),即所有尾部匹配的字符串。注意,"MPLE"、"PLE"、"LE"、"E"都是好后缀。

9.

比较前一位,发现"I"与"A"不匹配。所以,"I"是"坏字符"。

10.

根据"坏字符规则",此时搜索词应该后移 2 - (-1)= 3 位。问题是,此时有没有更好的移法?

11.

我们知道,此时存在"好后缀"。所以,可以采用"好后缀规则"

  后移位数 = 好后缀的位置 - 搜索词中的上一次出现位置

举例来说,如果字符串"ABCDAB"的后一个"AB"是"好后缀"。那么它的位置是5(从0开始计算,取最后的"B"的值),在"搜索词中的上一次出现位置"是1(第一个"B"的位置),所以后移 5 - 1 = 4位,前一个"AB"移到后一个"AB"的位置。

再举一个例子,如果字符串"ABCDEF"的"EF"是好后缀,则"EF"的位置是5 ,上一次出现的位置是 -1(即未出现),所以后移 5 - (-1) = 6位,即整个字符串移到"F"的后一位。

这个规则有三个注意点:

  (1)"好后缀"的位置以最后一个字符为准。假定"ABCDEF"的"EF"是好后缀,则它的位置以"F"为准,即5(从0开始计算)。

  (2)如果"好后缀"在搜索词中只出现一次,则它的上一次出现位置为 -1。比如,"EF"在"ABCDEF"之中只出现一次,则它的上一次出现位置为-1(即未出现)。

  (3)如果"好后缀"有多个,则除了最长的那个"好后缀",其他"好后缀"的上一次出现位置必须在头部。比如,假定"BABCDAB"的"好后缀"是"DAB"、"AB"、"B",请问这时"好后缀"的上一次出现位置是什么?回答是,此时采用的好后缀是"B",它的上一次出现位置是头部,即第0位。这个规则也可以这样表达:如果最长的那个"好后缀"只出现一次,则可以把搜索词改写成如下形式进行位置计算"(DA)BABCDAB",即虚拟加入最前面的"DA"。

回到上文的这个例子。此时,所有的"好后缀"(MPLE、PLE、LE、E)之中,只有"E"在"EXAMPLE"还出现在头部,所以后移 6 - 0 = 6位。

12.

可以看到,"坏字符规则"只能移3位,"好后缀规则"可以移6位。所以,Boyer-Moore算法的基本思想是,每次后移这两个规则之中的较大值。

更巧妙的是,这两个规则的移动位数,只与搜索词有关,与原字符串无关。因此,可以预先计算生成《坏字符规则表》和《好后缀规则表》。使用时,只要查表比较一下就可以了。

13.

继续从尾部开始比较,"P"与"E"不匹配,因此"P"是"坏字符"。根据"坏字符规则",后移 6 - 4 = 2位。

14.

从尾部开始逐位比较,发现全部匹配,于是搜索结束。如果还要继续查找(即找出全部匹配),则根据"好后缀规则",后移 6 - 0 = 6位,即头部的"E"移到尾部的"E"的位置。


附上Boyer-Moore模板(链接:Seiyagoo

<span style="font-size:18px;">#include <stdio.h>

#include <stdint.h>

#include <stdlib.h>
 

#define ALPHABET_LEN 256

uint32_t patlen;

#define NOT_FOUND patlen

#define max(a, b) ((a < b) ? b : a)
 
/*构造Bc表*/

void make_delta1(int *delta1, uint8_t *pat, int32_t patlen) {
    int i;
     

    /*初始化整个字符表的shift值为模式串P的长度(即case 2:出现坏字符时,P中无相同的字符)*/

    for (i=0; i < ALPHABET_LEN; i++) {

        delta1[i] = NOT_FOUND;
    }

    /*从左至右更新相同字符离失配位置(即patlen-1)的最近距离(case 1)*/

    for (i=0; i < patlen-1; i++) {

        delta1[pat[i]] = patlen-1 - i;
    }
}
 

/*Gs规则case 2:suffix-prefix对,从已匹配后缀[pos, wordlen)判断word是否存在前缀,

   即word[0, suffixlen) == word[pos, wordlen)
*/

int is_prefix(uint8_t *word, int wordlen, int pos) {
    int i;

    int suffixlen = wordlen - pos;

    // could also use the strncmp() library function here

    for (i = 0; i < suffixlen; i++) {

        if (word[i] != word[pos+i]) {

            return 0;
        }
    }
    return 1;
}
 

/*Gs规则case 1:suffix-suffix对,从pos向←查找与从P末尾(即已匹配后缀)向←查找相等的最长后缀,

   并返回最长后缀的长度
 */

int suffix_length(uint8_t *word, int wordlen, int pos) {
    int i;

    // increment suffix length i to the first mismatch or beginning of the word

    //比较范围[1, pos]与[patlen-pos, patlen-1], 注意:串word[0..pos]的后缀不包含自身

    for (i = 0; (word[pos-i] == word[wordlen-1-i]) && (i < pos); i++);
    return i;
}
 
/*构造Gs表*/

void make_delta2(int *delta2, uint8_t *pat, int32_t patlen) {
    int p;

    int last_prefix_index = patlen-1;
 

    /*first loop:Gs规则case 2*/

    for (p=patlen-1; p>=0; p--) {

        if (is_prefix(pat, patlen, p+1)) { //从p+1开始的后缀是否存在前缀(p失配)

            last_prefix_index = p+1; //last_prefix_index记录从右至左最后一个匹配字符的index(即p的右边)
        }

        //若存在前缀,保存最后一个匹配字符的index;否则,保存上次已匹配字符的index

        delta2[p] = last_prefix_index;                    //@bug 1: + (patlen-1 - p);
    }
 

    /*
   second loop:Gs规则case 1,因为case 2是前缀,而中间的子串(可以看做[0,p]的suffix)也可能=P的suffix,

   且有可能不止一个中间子串,故p从左向后进行处理,保存最靠近P的suffix的对应子串前一个字符的shift长度
    */

    for (p=0; p < patlen-1; p++) {

        int slen = suffix_length(pat, patlen, p);         //末尾向左对应的从p向左的最长后缀的长度

         /*
       若已匹配suffix-suffix对的前导字符不匹配,保存向左的第一个失配字符的shift长度
      (即suffix-suffix对的起始位置之差)。若匹配,则为前缀即case 2,无需改变shift值
         */

        if (slen > 0 && pat[p - slen] != pat[patlen-1 - slen]) {   
          //slen=0, 即case 3:delta2[patlen-1-slen]=delta2[patlen-1]=patlen

            delta2[patlen-1 - slen] = patlen-1 - p ;       //@bug 2: + slen;
        }
    }
}
 

/*打印预处理得到的Bc表和Gs表*/

void print_pre_table(int *delta1, int *delta2, uint8_t *pat, uint32_t patlen){

      uint32_t i;

      printf("模式串:%s\n", pat);

      printf("坏字符shift表:\n");

      for (i=0; i < patlen-1; i++) {

          printf("(%c, %d)\n", pat[i], delta1[pat[i]]);
      }

      printf("(其他字符, %d)\n", NOT_FOUND);
 

      printf("\n好后缀shift表:\n");

      for (i=0; i < patlen; i++) {

           printf("(%u, %d)\n", i, delta2[i]);
      }
}
 

/*BM算法主框架*/

uint8_t boyer_moore (uint8_t *string, uint32_t stringlen, uint8_t *pat, uint32_t patlen) {

    uint32_t i;

    int delta1[ALPHABET_LEN];

    int *delta2 = (int *)malloc(patlen * sizeof(int));

    make_delta1(delta1, pat, patlen);

    make_delta2(delta2, pat, patlen);

    print_pre_table(delta1, delta2, pat, patlen);
 

    i = patlen-1;

    while (i < stringlen) {

        int j = patlen-1;

        while (j >= 0 && (string[i] == pat[j])) {

            --i;

            --j;
        }

        if (j < 0) {

            free(delta2);

      return i+1;       //返回T中匹配的位置
        }
 

        i += max(delta1[string[i]], delta2[j]);   
           //j失配( [j+1, patlen)已匹配 ),
          //  i向右移动的距离取主串T中坏字符delta1[string[i]]与模式串P中好后缀delta2[j]的大者
    }

    free(delta2);
return -1;
}
 
int main()
{

uint8_t pat[]="abracadabra";

uint8_t txt[]="abracadabtabradabracadabcadaxbrabbracadabraxxxxxxabracadabracadabra";

patlen = sizeof(pat)/sizeof(pat[0]) - 1;

uint32_t n = sizeof(txt)/sizeof(txt[0]) - 1;
 

uint8_t ans=boyer_moore(txt, n, pat, patlen);

printf("\n匹配位置:%d\n", ans);
return 0;
}
</span>



  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值