字符串搜索算法

单模式字符串匹配

1. 朴素算法

朴素算法的问题在于不够智能,有些位置明显没有必要进行比较操作,但这个算法无法区分出来,还是继续比较,浪费了资源。

2. KMP算法

在KMP算法中,引入了前缀函数的概念,从而可以更加精确的知道:当不匹配发生时,应该跳过多少个字符。下面介绍前缀函数。

字符串A = "abcde" B = "ab"。 那么就称字符串B为A的前缀,记为B ⊏ A。同理可知 C = "e","de" 等都是 A 的后缀,以为C ⊐ A。

 

这里模式串 P = “ababaca”,在匹配了 q=5 个字符后失配,因此,下一步就是要考虑将P向右移多少位进行新的一轮匹配检测。朴素算法中,直接将P右移1位,也就是将P的首字符'a'去和目标串的'b'字符进行检测,这明显是多余的。通过我们肉眼的观察,可以很简单的知道应该将模式串P右移到下图'a3'处再开始新一轮的检测,直接跳过肯定不匹配的字符'b',那么我们“肉眼”观察的这一结果怎么把它用语言表示出来呢?

 

我们的观察过程是这样的:
1. P的前缀"ab"中'a' != 'b',又因该前缀已经匹配了T中对应的"ab",因此,该前缀的字符'a1'肯定不会和T中对应的字串"ab"中的'b'匹配,也就是将P向右滑动一个位移是无意义的。
2. 接下来考察P的前缀"aba",发现该前缀自身的前缀'a1'与自身后缀'a2'相等,"a1 b a2" 已经匹配了T中的"a b a3",因此有 'a2' == 'a3', 故得到 'a1' == 'a3'......
3. 利用此思想,可推知在已经匹配 q=5 个字符的情况下,将P向右移 当且仅当 2个位移时,才能满足既没有冗余(如把'a'去和'b'比较),又不会丢失(如把'a1' 直接与 'a4' 开始比较,则丢失了与'a3'的比较)。
4. 而前缀函数就是这样一种函数,它决定了q与位移的一一对应关系,通过它就可以间接地求得位移s。

这样的观察过程并不具有一般性,下面是《算法导论》中对前缀函数的形式化说明:

已知一个模式P[1. . m],模式P的前缀函数是函数π{1,2,. . . , m}->{0,1, 2,. . . ,m-1}并满足

π[q]=max{k:k<q 且Pk⊐ Pq}

即π[q]是Pq的真后缀P的最长前缀的长度(此是《算法导论》中原话,但不是很好理解,其实就是Pq中即是自己的真后缀,又是自己最长前缀的字符串的最大长度)。下面举例说明(模式P=ababababca)

i=1时,a真后缀为空;i=2时,ab真后缀为b,不是自己的前缀;i=3时,aba真后缀为a, ab,且a和ab都是aba的前缀,ab最长,故为2;。。。

KMP算法中,如果q+1时发生不匹配,则可以向前移动q-π[q]位。

#include <iostream>

using namespace std;
/*
when searching a pattern in a string, and mismatch happened, we can skip more chars, instead of going through one by one;
the skip rule is that:
1. if position p mismatched, we need consider the chars in 0- (p-1);
2. whether [0,k-1](prefix substring) matched with [p-k,p-1](suffix substring),if matched, we can align the pattern to p-k;and do comparation from p again.
below function is get the k for different p, more information refer to comments inline;
*/
void get_skippattern(char *pattern, int* next, int len)
{
    int pos = 2;
    int subStrIndex = 0; //valid prefix candidate substring index;
    next[0] = -1; // when 1st char mismatched, always move 1 (p=0, k=-1);
    next[1] = 0// when 2nd mismatched, always move 1(p=1, k=0);in fact, if the 2nd char is same as 1st char, we can move 2
    while(pos<len)
    {
        if(pattern[pos - 1] == pattern[subStrIndex]) //one char matched, then continue to match more,
        {
            subStrIndex++;            //prefix substring move ahead;
            next[pos] = subStrIndex;//for current position, the k is got;
            pos++;                    //current pos move ahead;
        }
        else if(subStrIndex>0)    //one substring found, but in the new pos, mismatched;
        {
            subStrIndex = next[subStrIndex]; //then we need fall back subStrIndex to value that still can be matched;
        }
        else
        {
            next[pos] = 0;
            pos++;
        }
    }

    for(int i =0;i<len;i++)
        cout<<next[i]<<"  ";
}

int KMP_search(char *src, int slen, char *pattern, int plen)
{
    int* next = (int *)malloc(sizeof(int)*slen);
    get_skippattern(pattern,next,plen);

    int indexInSrc = 0;
    int offset = 0;
    while((indexInSrc+offset)<slen)
    {
        if(pattern[offset] == src[indexInSrc+offset])
        {
            if(offset == (plen-1))
                return indexInSrc;
            offset++;
        }else
        {
            indexInSrc += offset-next[offset];
            if(next[offset]>-1)
                offset = next[offset];
            else
                offset = 0;
        }
    }
    return slen;
}

int main (int argc, char ** argv)
{
    //char *pat = "ABCDABDEF";
   
//char *src = "ABC ABCDAB ABCDABCDABDEF";
    char *pat="ABABETTABABABYUABCD";
    char *src = "ABCDEABCDGABCDETTABCDFABCDETTABCDATYUABCD";
    int index = KMP_search(src,strlen(src),pat,strlen(pat));
    cout<<"found pattern in "<<index<<endl;
    return 0;
}

3. BM算法

BM算法的特殊之处在于BM是右向左匹配,同时结合坏字符和好后缀两个规则使得移动距离最大。下面分别介绍坏字符和好后缀规则:

好后缀算法

如果程序匹配了一个好后缀, 并且在模式中还有另外一个相同的后缀, 那

把下一个后缀移动到当前后缀位置。好后缀算法有两种情况:

Case1:模式串中有子串和好后缀安全匹配,则将最靠右的那个子串移动到好后缀的位置。继续进行匹配。

wps_clip_image-979

Case2:如果不存在和好后缀完全匹配的子串,则在好后缀中找到具有如下特征的最长子串,使得P[m-s…m]=P[0…s]。说不清楚的看图。

wps_clip_image-1152

给一些具体的例子

坏字符算法

当出现一个坏字符时, BM算法向右移动模式串, 让模式串中最靠右的对应字符与坏字符相对,然后继续匹配。坏字符算法也有两种情况。

Case1:模式串中有对应的坏字符时,见图。
wps_clip_image-1349

Case2:模式串中不存在坏字符。见图。

wps_clip_image-1472

移动规则

BM算法的移动规则是:

将概述中的++j,换成j+=MAX(shift(好后缀),shift(坏字符)),即BM算法是每次向右移动模式串的距离是,按照好后缀算法和坏字符算法计算得到的最大值

shift(好后缀)和shift(坏字符)通过模式串的预处理数组的简单计算得到。好后缀算法的预处理数组是bmGs[],坏字符算法的预处理数组是BmBc[]。

下面先解释这两个数组的意义:

BmBc 的定义:

1、 字符在模式串中没有出现:,如模式串中没有字符p,则BmBc[‘p’] = strlen(模式串)。

2、 字符在模式串中有出现。如下图,BmBc[‘k’]表示字符k在模式串中最后一次出现的位置,距离模式串串尾的长度。

wps_clip_image-1885

如果只考虑坏字符,应该移动多少呢?下面的图里有3个例子:

示例1中,在b和c比较时发生了不match,这时,我们的BmBc[‘c’] = 3,这时我们应该移动多少呢,移动-1,如何计算的呢? BmBc[‘c’] – strlen(pat) +1 + i (index of pattern string)

实例2中,b和a发生不match,这时,BmBc[‘a’] = 6, 应该移动6-7+1+2 = 2;

实例3中,b和y发生不match,这时,BmBc[‘y’] = 7,应该移动7-7+1+1 = 2;

对于这里BmBc的定义,和shift的值的计算是很难让人理解的,我们是不是可以简单一点定义BmBc[char] 表示char在pattern中最后出现的位置,如果不出现为pattern的长度,i是不match的index,shift的距离就是BmBc[char]-i。

还有就是,在实例1中,我们真的要去移动-1吗,其实没有必要了,如果只用坏字符,你可以想想怎么做;但如果考虑上好后缀就不用额外考虑了。

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

计算suffix的代码如下所示:

suffix[m-1]=m;
for (i=m-2;i>=0;--i)
{   
    q=i;  
    while(q>=0&&P[q]==P[m-1-i+q])       
        --q;   
    suffix[i]=i-q;
}

 

有了suffix[i],如何计算BmGc?

bmGs的定义(BmGs数组的下标是数字,表示字符在模式串中位置), BmGs数组的定义,分三种情况:

  • 模式串中有子串匹配上好后缀

在这个视角图1中,在i处发生不匹配,从i开始从右向左搜索子字符串,在视图2中试图找到不匹配的字符和子串匹配位置的关系。视图2中i开始的子串与后缀匹配,那可以知道后缀的长度就是Suffix[i],再往左移动一下就是不匹配的位置了,而这时应该移动的距离是m-1-i,也就是式子bmGs[m-1-suff[i]] = m- 1 – i;

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

在这种情况下,空白位置发生不匹配时,其好后缀都是最前面的两个,那么其移动的距离其实跟不匹配的位置 j 没有关系,只与最好前缀的位置i有关,所以,bmGs[j] = m- 1 – i;

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

没有任何子串匹配的时候,那就移动模式串的长度。

举例如下:

wps_clip_image-2380

实现代码如下:

void preBmGs(char *x, int m, int bmGs[]) { 
    int i, j, suff[XSIZE];
    suffixes(x, m, suff);  
    for (i = 0; i < m; ++i)
        bmGs[i] = m;  
    j = 0;
    for (i = m - 1; i >= 0; --i)     
        if (suff[i] == i + 1)        
            for (; j < m - 1 - i; ++j)           
                if (bmGs[j] == m)              
                    bmGs[j] = m - 1 - i;  
    for (i = 0; i <= m - 2; ++i)     
        bmGs[m - 1 - suff[i]] = m - 1 - i;
}

下面来完整实现一下BM算法吧:

#include <stdlib.h>
#include <stdio.h>
#include <string.h>


const int CHAR_COUNT = 26// only lower case ASCII char
void calculateBmBc(const char *s, int len,  int *BmBc)
{
        int i=0;
        for(i = 0; i<CHAR_COUNT;i++)
        {
                BmBc[i] = len;
        }
        for(i = 0;i<len;i++)
        {
                BmBc[s[i]-'a'] = i;
        }
}
void calculateSuffix(const char *s, int len,int *suffix)
{
        int i = len -1;
        int j = 0;
        suffix[len-1] = len;
        for(;i>=0;i--)
        {
                j = 0;
                while(j<(len-1) && s[i-j] == s[len-1-j])
                        j++;
                suffix[i] = j;
        }
}
void calculateBmGs(const char *s, int len, int *BmGs)
{
        int* suffix = (int *)malloc(sizeof(int)*len);//new int[len];
        int i = 0;
        int j = 0;
        calculateSuffix(s,len,suffix);
        for(i=0;i<len;i++)// init the array, and also cover case 3
        {
                BmGs[i] = len;
        }
        for(i=len-1;i>=0;i--)
        {
                if(suffix[i] == i+1// prefix of the string matched with suffix, case 2
                {
                        for(j = 0;j<len-1-i;j++)
                        {
                                if(BmGs[j] == len)
                                        BmGs[j] = len - 1 - i;
                        }
                }
        }
        for(i = 0;i<=len-2;i++) // case 1;
        {
                BmGs[len-1-suffix[i]] = len-1-i;
        }
        free(suffix);
}
int BMSearch(const char *src,int srclen, const char *pattern, int patlen)
{
        int i= 0;
        int * BmGs = (int *)malloc(sizeof(int)*(patlen));
        int * BmBc = (int *)malloc(sizeof(int)*(CHAR_COUNT));
        calculateBmBc(pattern, patlen, BmBc);
        calculateBmGs(pattern,patlen, BmGs);
        for(i = 0;i<(srclen-patlen);)
        {
                int j = patlen-1;
                while(j>=0 && src[i+j] == pattern[j]) j--;
                if(j <0)
                        return i;
                else
                        i += BmGs[j]>(j-BmBc[j])?BmGs[j]:(j-BmBc[j]);
        }
        free(BmGs);
        free(BmBc);
        return srclen;
}

int main(int argc, char **argv)
{
        int i = BMSearch("GCAGAGAG",8,"AGAG",4);
        printf("found pattern at %d",i);
        return 0;
}

4. Sunday算法

多模式字符串匹配

1. AC

2. Wu-Manber算法

Reference:

1. http://blog.csdn.net/zdl1016/article/details/4654061

2. http://blog.csdn.net/iJuliet/article/details/4200771

3. http://quicksort.typepad.com/blog/2010/01/%E5%AD%97%E7%AC%A6%E4%B8%B2%E5%8C%B9%E9%85%8D%E7%9A%84sunday.html

4. http://hi.baidu.com/kmj0217/blog/item/6f837f2f3da097311e3089cb.html

5. http://www.cs.utexas.edu/users/moore/best-ideas/string-searching/index.html

6. http://blog.sina.com.cn/s/blog_6cf48afb0100n561.html

7. http://blog.csdn.net/sealyao/article/details/4568167

8. http://www.cnblogs.com/v-July-v/archive/2011/06/15/2084260.html

原文地址:https://www.cnblogs.com/whyandinside/p/2532651.html