1. 程式人生 > >字串匹配演算法

字串匹配演算法

字串匹配的意思是給一個字串集合,和另一個字串集合,看這兩個集合交集是多少。

若是都只有一個字串,那麼就看其中一個是否包含另外一個;

若是父串集合(比較長的,被當做模板)的有多個,子串(拿去匹配的)只有一個,就是問這個子串是否存在於父串之中;

若是子串父串集合都有多個,那麼就是問交集了。

1.KMP演算法

KMP演算法是用來處理一對一的匹配的。

樸素的匹配演算法,或者說暴力匹配法,就是將兩個字串從頭比到尾,若是有一個不同,那麼從下一位再開始比。這樣太慢了。所以KMP演算法的思想是,對匹配串本身先做一個處理,得到一個next陣列。這個陣列是做什麼用的呢?next [j] = k,代表j之前的字串中有最大長度為k 的相同字首字尾。記錄這個有什麼用呢?對於ABCDABC這個串,如果我們匹配ABCDABTBCDABC這個長串,當匹配到第7個字元T的時候就不匹配了,我們就不用直接移到B開始再比一次,而是直接移到第5位來比較,豈不美哉?所以求出了next陣列,KMP就完成了一大半。next陣列也可以說是開始比較的位數。

計算next陣列的方法是對於長度為n的匹配串,從0到n-1位依次求出字首字尾最大匹配長度。

比如ABCDABD這個串:

如何去求next陣列呢?k是匹配下標。這裡沒有從最後一位開始和第一位開始分別比較字首字尾,而是利用了next[i-1]的結果。

void getnext()//獲取next陣列 {     int i,n,k;     n=strlen(ptr);     memset(next,0,sizeof(next));     k=0;     for(i=1;i<n;i++)     {         while(k>0 && ptr[k]!=ptr[i])             k=next[k];         if(ptr[k]==ptr[i]) k++;         next[i+1]=k;     //next表示的是匹配長度     } }

這裡我是按照《演算法導論》的程式碼來寫的。演算法導論演算法迴圈是從1到n而不是從0到n-1,所以在下面匹配的時候需要j=next[j+1]。

int kmp(char *a,char *b)//匹配ab兩串,a為父串 {     int i=0,j=0;     int len1=strlen(a);     int len2=strlen(b);     getnext();     while(i<len1&&j<len2)     {         if(j==0||a[i]==b[j])         {   i++;j++;       }         else j=next[j+1];//到前一個匹配點     }     if(j>=len2)         return i-j;     else return -1; }

這裡next陣列的作用就顯現出來了。最後返回的是i-j,也就是說,是從i位置前面的第j位開始的,也就是上面說的,next陣列也可以說是開始比較的位數。也就是說,在父串的i位比的時候已經是在比子串的第j位了。

一個完整的程式碼:

#include <iostream> #include <cstring> #include <cstdio> using namespace std; const int N=100; char str[100],ptr[100];//父串str和子串ptr int next[100]; string ans; void getnext()//獲取next陣列 {     int i,n,k;     n=strlen(ptr);     memset(next,0,sizeof(next));     k=0;     for(i=1;i<n;i++)     {         while(k>0 && ptr[k]!=ptr[i])             k=next[k];         if(ptr[k]==ptr[i]) k++;         next[i+1]=k;     //next表示的是匹配長度     } } int kmp(char *a,char *b)//匹配ab兩串,a為父串 {     int i=0,j=0;     int len1=strlen(a);     int len2=strlen(b);     getnext();     while(i<len1&&j<len2)     {         if(j==0||a[i]==b[j])         {   i++;j++;       }         else j=next[j+1];//到前一個匹配點     }     if(j>=len2)         return i-j;     else return -1; } int main(){     while( scanf( "%s%s", str, ptr ) )     {         int ans = kmp(str,ptr);         if(ans>=0)             printf( "%d\n", kmp( str,ptr ));         else             printf("Not find\n");     }     return 0; }

2.字典樹演算法

上面的KMP是一對一匹配的時候常用的演算法。而字典樹則是一對多的時候匹配常用演算法。其含義是,把一系列的模板串放到一個樹裡面,然後每個節點存的是它自己的字元,從根節點開始往下遍歷就可以得到一個個單詞了。

我這裡寫的程式碼稍微和上面有一點區別,我的節點tnode裡面沒有存它本身的字元,而是存一個孩子陣列。所以當資料量很大的時候還是需要做一些變通的,不可直接套用此程式碼。若是想以每個節點為一個node,那麼要注意根節點是空的。

樹的節點tnode,這裡的next[i]存的是子節點指標。sum=0表示這個點不是重點。為n>0表示有n個單詞以此為終點。

struct tnode{     int sum;//用來判斷是否是終點的     tnode* next[26];     tnode(){         for(int i =0;i<26;i++)             next[i]=NULL;         sum=0;     } };

插入函式:

這個newnode是手寫的建構函式.C++類有些坑,不像java那麼...隨便。

假設字典樹已經有了aer,現在插入abc,首先看a,不為空,那麼直接跳到a節點裡,看b,為空,那麼新建,跳到b裡,新建c,跳出。

tnode* newnode(){     tnode *p = new tnode;     for(int i =0;i<26;i++)         p->next[i]=NULL;     p->sum=0;     return p; } //插入函式 void Insert(char *s) {     tnode *p = root;     for(int i = 0 ; s[i] ; i++)     {         int x = s[i] - 'a';         if(p->next[x]==NULL)         {             tnode *nn=newnode();             for(int j=0;j<26;j++)                 nn->next[j] = NULL;             nn->sum = 0;             p->next[x]=nn;         }         p = p->next[x];     }     p->sum++;//這個單詞終止啦 }

字串比較:就是一個個字元去比唄...時間複雜度O(m),m是匹配串長度。

bool Compare(char *ch) {     tnode *p = root;     int len = strlen(ch);     for(int i = 0; i < len; i++)     {         int x = ch[i] - 'a';         p = p->next[x];         if(p==NULL)             return false;         if(i==len-1 && p->sum>0 ){             return true;         }     }     return false; }

給個完整的程式碼:

#include<queue> #include<set> #include<cstdio> #include <iostream> #include<algorithm> #include<cstring> #include<cmath> using namespace std; /*     trie字典樹 */ struct tnode{     int sum;//用來判斷是否是終點的     tnode* next[26];     tnode(){         for(int i =0;i<26;i++)             next[i]=NULL;         sum=0;     } }; tnode *root;

tnode* newnode(){     tnode *p = new tnode;     for(int i =0;i<26;i++)         p->next[i]=NULL;     p->sum=0;     return p; } //插入函式 void Insert(char *s) {     tnode *p = root;     for(int i = 0 ; s[i] ; i++)     {         int x = s[i] - 'a';         if(p->next[x]==NULL)         {             tnode *nn=newnode();             for(int j=0;j<26;j++)                 nn->next[j] = NULL;             nn->sum = 0;             p->next[x]=nn;         }         p = p->next[x];     }     p->sum++;//這個單詞終止啦 } //匹配函式 bool Compare(char *ch) {     tnode *p = root;     int len = strlen(ch);     for(int i = 0; i < len; i++)     {         int x = ch[i] - 'a';         p = p->next[x];         if(p==NULL)             return false;         if(i==len-1 && p->sum>0 ){             return true;         }     }     return false; } void DELETE(tnode * &top){     if(top==NULL)     return;     for(int i =0;i<26;i++)         DELETE(top->next[i]);     delete top; } int main() {     int n,m;     cin>>n;     char s[20];     root = newnode();     for(int i =0;i<n;i++){         scanf("%s",s);         Insert(s);     }     cin>>m;     for(int i =0;i<m;i++){         scanf("%s",s);         if(Compare(s))             cout<<"YES"<<endl;         else             cout<<"NO"<<endl;     }     DELETE(root);//看見指標就要想到釋放,然而這東西會花時間,所以網上很多人寫ACM題就不delete了,我很看不慣這一點。     return 0; }

3.AC自動機

字典樹是一對多的匹配,那麼AC自動機就是多對多的匹配了。意思是:給一個字典,再給一個m長的文字,問這個文本里出現了字典裡的哪些字。

這個問題可以用n個單詞的n次KMP演算法來做(效率為O(n*m*單詞平均長度)),也可以用1個字典樹去匹配文字串的每個字母位置來做(效率為O(m*每次字典樹遍歷的平均深度))。上面兩種解法效率都不高,如果用AC自動機來解決的話,效率將為線性O(m)時間複雜度。

AC自動機也運用了一點KMP演算法的思想。簡述為字典樹+KMP也未為不可。

首先講一下acnode的結構:

與字典樹相比,就多了個*fail對吧,這個就相當於KMP演算法裡的next陣列。只不過它存的是失配後跳轉的位置,而不是跳轉之後再向前跳了多少罷了。

struct acnode{     int sum;     acnode* next[26];     acnode* fail;     acnode(){         for(int i =0;i<26;i++)             next[i]=NULL;         fail= NULL;         sum=0;     } };

插入什麼的我就不說了,記得把fail置為空即可。

這裡說一下fail指標的獲取。fail指標是通過BFS來求的。

看這麼一張圖

圖中數字我們不用管它,綠色代表是終點,虛線就是fail指標了。我們可以看到91 E節點的fail指標是指向76 E 的,也就是說執行到這裡如果無法繼續匹配就會跳到76 E那個節點繼續往後匹配。我們可以看到它們前面都是H,也就是說fail指標指向的是父節點相同的同值節點(根節點視為與任何節點相同)。我們要算的是在一個長文本里面有多少個出現的單詞,這個fail指標就是為了快速匹配而誕生的。若文本里出現了HISHERS,我們首先匹配了HIS,有通過fail指標跳到85 S從而匹配SHE,再匹配HERS。fail指標跳到哪裡就代表這一點之前的內容已經被匹配了。這樣就避免了再從頭重複判斷的過程。

在函式裡,當前節點的fail指標也會去更新此節點的孩子的fail指標,因為父節點相同啊~而且因為它是此節點的fail指標,這兩個節點的父節點也相同啊~所以一路相同過來,就保證fail指向的位置字首是相同的。

void getfail(){     queue<acnode*> q;     for(int i = 0 ; i < 26 ; i ++ )     {         if(root->next[i]!=NULL){             root->next[i]->fail = root;             q.push(root->next[i]);         }     }     while(!q.empty()){         acnode* tem = q.front();         q.pop();         for(int i = 0;i<26;i++){             if(tem->next[i]!=NULL)             {                 acnode *p;                 p = tem->fail;                 while(p!=NULL){                     if(p->next[i]!=NULL){                         tem->next[i]->fail = p->next[i];                         break;                     }                     p=p->fail;                 }                 if(p==NULL)                    tem->next[i]->fail = root;                 q.push(tem->next[i]);             }         }     } }

全部程式碼如下:

#include<queue> #include<set> #include<cstdio> #include <iostream> #include<algorithm> #include<cstring> #include<cmath> using namespace std; /*     ac自動機 */ struct acnode{     int sum;     acnode* next[26];     acnode* fail;     acnode(){         for(int i =0;i<26;i++)             next[i]=NULL;         fail= NULL;         sum=0;     } }; acnode *root; int cnt; acnode* newnode(){     acnode *p = new acnode;     for(int i =0;i<26;i++)         p->next[i]=NULL;     p->fail = NULL;     p->sum=0;     return p; } //插入函式 void Insert(char *s) {     acnode *p = root;     for(int i = 0; s[i]; i++)     {         int x = s[i] - 'a';         if(p->next[x]==NULL)         {             acnode *nn=newnode();             for(int j=0;j<26;j++)                 nn->next[j] = NULL;             nn->sum = 0;             nn->fail = NULL;             p->next[x]=nn;         }         p = p->next[x];     }     p->sum++; } //獲取fail指標,在插入結束之後使用 void getfail(){     queue<acnode*> q;     for(int i = 0 ; i < 26 ; i ++ )     {         if(root->next[i]!=NULL){             root->next[i]->fail = root;             q.push(root->next[i]);         }     }     while(!q.empty()){         acnode* tem = q.front();         q.pop();         for(int i = 0;i<26;i++){             if(tem->next[i]!=NULL)             {                 acnode *p;                 if(tem == root){                     tem->next[i]->fail = root;                 }                 else                 {                     p = tem->fail;                     while(p!=NULL){                         if(p->next[i]!=NULL){                             tem->next[i]->fail = p->next[i];                             break;                         }                         p=p->fail;                     }                     if(p==NULL)                         tem->next[i]->fail = root;                 }                 q.push(tem->next[i]);             }         }     } } //匹配函式 void ac_automation(char *ch) {     acnode *p = root;     int len = strlen(ch);     for(int i = 0; i < len; i++)     {         int x = ch[i] - 'a';         while(p->next[x]==NULL && p != root)//沒匹配到,那麼就找fail指標。             p = p->fail;         p = p->next[x];         if(!p)             p = root;         acnode *temp = p;         while(temp != root)         {            if(temp->sum >= 0)             /*             在這裡已經匹配成功了,執行想執行的操作即可,怎麼改看題目需求+             */            {                cnt += temp->sum;                temp->sum = -1;            }            else break;            temp = temp->fail;         }     } }

int main() {     cnt = 0;     int n;     cin>>n;     char c[101];     root = newnode();     for(int i = 0 ;i < n;i++){         scanf("%s",c);         Insert(c);     }     getfail();     int m ;     cin>> m;     for(int i = 0;i<m;i++){         scanf("%s",c);         ac_automation(c);     }     cout<<cnt<<endl;     return 0; }