字符串搜索算法


單模式字符串匹配

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM