zoukankan      html  css  js  c++  java
  • 字符串搜索算法

    单模式字符串匹配

    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

  • 相关阅读:
    C#-使用Tuple传递多个参数
    CentOS 常用命令
    C#-ToString格式化
    java面对对象(六)--内部类、匿名内部类
    JAVA面对对象(五)——接口
    JAVA面对对象(四)——抽象类
    JAVA面对对象(三)——Super、static、final关键字
    Mybatis缓存
    重启博客
    某大神的装修笔记
  • 原文地址:https://www.cnblogs.com/whyandinside/p/2532651.html
Copyright © 2011-2022 走看看